Add reasoning-only status surface item (#25504)

Closes #24886.

## Why
Users can configure the TUI status line and terminal title with
`model-with-reasoning`, but issue #24886 asks for a compact
reasoning-only item. That lets a setup show just `default`, `low`,
`medium`, `high`, or `xhigh` without repeating the model name.

## What changed
- Added a `reasoning` item for `/statusline` and `/title` setup flows.
- Rendered the item from the effective reasoning effort, including
collaboration-mode overrides.
- Registered `reasoning` with `codex doctor` so Codex-generated
terminal-title config is not reported as invalid.
- Updated TUI setup snapshots so the picker previews include the new
item.
This commit is contained in:
Eric Traut
2026-06-01 09:30:20 -07:00
committed by GitHub
parent 6681446477
commit f1d029cf75
12 changed files with 83 additions and 7 deletions

View File

@@ -143,6 +143,7 @@ fn terminal_title_item_id(item: &str) -> Option<&'static str> {
"fast-mode" => Some("fast-mode"),
"model" | "model-name" => Some("model"),
"model-with-reasoning" => Some("model-with-reasoning"),
"reasoning" => Some("reasoning"),
"task-progress" => Some("task-progress"),
_ => None,
}

View File

@@ -14,8 +14,8 @@ expression: "render_lines(&view, 72)"
[x] current-dir Current working directory
[x] git-branch Current Git branch (omitted when unavaila…
[ ] model-with-reasoning Current model name with reasoning level
[ ] reasoning Current reasoning level
[ ] project-name Project name (omitted when unavailable)
[ ] pull-request-number Open pull request number for the current …
gpt-5-codex · ~/codex-rs · jif/statusline-preview
Press space to toggle; ←/→ to move; enter to confirm and close; esc to

View File

@@ -60,6 +60,9 @@ pub(crate) enum StatusLineItem {
/// Model name with reasoning level suffix.
ModelWithReasoning,
/// Current reasoning level.
Reasoning,
/// Current working directory path.
CurrentDir,
@@ -144,6 +147,7 @@ impl StatusLineItem {
match self {
StatusLineItem::ModelName => "Current model name",
StatusLineItem::ModelWithReasoning => "Current model name with reasoning level",
StatusLineItem::Reasoning => "Current reasoning level",
StatusLineItem::CurrentDir => "Current working directory",
StatusLineItem::ProjectRoot => "Project name (omitted when unavailable)",
StatusLineItem::GitBranch => "Current Git branch (omitted when unavailable)",
@@ -191,6 +195,7 @@ impl StatusLineItem {
match self {
StatusLineItem::ModelName => StatusSurfacePreviewItem::Model,
StatusLineItem::ModelWithReasoning => StatusSurfacePreviewItem::ModelWithReasoning,
StatusLineItem::Reasoning => StatusSurfacePreviewItem::Reasoning,
StatusLineItem::CurrentDir => StatusSurfacePreviewItem::CurrentDir,
StatusLineItem::ProjectRoot => StatusSurfacePreviewItem::ProjectRoot,
StatusLineItem::GitBranch => StatusSurfacePreviewItem::GitBranch,
@@ -450,6 +455,15 @@ mod tests {
);
}
#[test]
fn reasoning_is_selectable_id() {
assert_eq!(StatusLineItem::Reasoning.to_string(), "reasoning");
assert_eq!(
"reasoning".parse::<StatusLineItem>(),
Ok(StatusLineItem::Reasoning)
);
}
#[test]
fn run_state_is_canonical_and_accepts_status_legacy_id() {
assert_eq!(StatusLineItem::Status.to_string(), "run-state");

View File

@@ -30,7 +30,9 @@ enum StatusLineAccent {
impl StatusLineAccent {
fn for_item(item: StatusLineItem) -> Self {
match item {
StatusLineItem::ModelName | StatusLineItem::ModelWithReasoning => Self::Model,
StatusLineItem::ModelName
| StatusLineItem::ModelWithReasoning
| StatusLineItem::Reasoning => Self::Model,
StatusLineItem::CurrentDir | StatusLineItem::ProjectRoot => Self::Path,
StatusLineItem::GitBranch
| StatusLineItem::PullRequestNumber

View File

@@ -32,6 +32,7 @@ pub(crate) enum StatusSurfacePreviewItem {
RawOutput,
Model,
ModelWithReasoning,
Reasoning,
TaskProgress,
}
@@ -63,6 +64,7 @@ impl StatusSurfacePreviewItem {
StatusSurfacePreviewItem::RawOutput => "raw output",
StatusSurfacePreviewItem::Model => "gpt-5.2-codex",
StatusSurfacePreviewItem::ModelWithReasoning => "gpt-5.2-codex medium",
StatusSurfacePreviewItem::Reasoning => "medium",
StatusSurfacePreviewItem::TaskProgress => "Tasks 0/0",
}
}
@@ -94,6 +96,7 @@ impl StatusSurfacePreviewItem {
Self::RawOutput,
Self::Model,
Self::ModelWithReasoning,
Self::Reasoning,
Self::TaskProgress,
]
.into_iter()

View File

@@ -82,6 +82,8 @@ pub(crate) enum TerminalTitleItem {
Model,
/// Current model name with reasoning level.
ModelWithReasoning,
/// Current reasoning level.
Reasoning,
/// Latest checklist task progress from `update_plan` (if available).
TaskProgress,
}
@@ -122,6 +124,7 @@ impl TerminalTitleItem {
TerminalTitleItem::FastMode => "Whether Fast mode is currently active",
TerminalTitleItem::Model => "Current model name",
TerminalTitleItem::ModelWithReasoning => "Current model name with reasoning level",
TerminalTitleItem::Reasoning => "Current reasoning level",
TerminalTitleItem::TaskProgress => {
"Latest task progress from update_plan (omitted until available)"
}
@@ -153,6 +156,7 @@ impl TerminalTitleItem {
TerminalTitleItem::ModelWithReasoning => {
Some(StatusSurfacePreviewItem::ModelWithReasoning)
}
TerminalTitleItem::Reasoning => Some(StatusSurfacePreviewItem::Reasoning),
TerminalTitleItem::TaskProgress => Some(StatusSurfacePreviewItem::TaskProgress),
}
}
@@ -516,6 +520,15 @@ mod tests {
);
}
#[test]
fn reasoning_is_selectable_id() {
assert_eq!(TerminalTitleItem::Reasoning.to_string(), "reasoning");
assert_eq!(
"reasoning".parse::<TerminalTitleItem>(),
Ok(TerminalTitleItem::Reasoning)
);
}
#[test]
fn parse_terminal_title_items_accepts_kebab_case_variants() {
let items = parse_terminal_title_items(
@@ -530,6 +543,7 @@ mod tests {
"project-name",
"model",
"model-with-reasoning",
"reasoning",
"weekly-limit",
"codex-version",
"used-tokens",
@@ -553,6 +567,7 @@ mod tests {
TerminalTitleItem::Project,
TerminalTitleItem::Model,
TerminalTitleItem::ModelWithReasoning,
TerminalTitleItem::Reasoning,
TerminalTitleItem::WeeklyLimit,
TerminalTitleItem::CodexVersion,
TerminalTitleItem::UsedTokens,

View File

@@ -14,7 +14,7 @@ expression: status_line_popup_snapshot(&mut chat)
[x] thread-title Current thread title, or thread identifier when unnamed
[ ] model Current model name
[ ] model-with-reasoning Current model name with reasoning level
[ ] current-dir Current working directory
[ ] reasoning Current reasoning level
my-project · feat/awesome-feature · thread title
Press space to toggle; ←/→ to move; enter to confirm and close; esc to close

View File

@@ -14,7 +14,7 @@ expression: status_line_popup_snapshot(&mut chat)
[x] thread-title Current thread title, or thread identifier when unnamed
[ ] model Current model name
[ ] model-with-reasoning Current model name with reasoning level
[ ] current-dir Current working directory
[ ] reasoning Current reasoning level
preview-live-root · feature/live-preview-branch · Live preview thread
Press space to toggle; ←/→ to move; enter to confirm and close; esc to close

View File

@@ -14,7 +14,7 @@ expression: status_line_popup_snapshot(&mut chat)
[x] thread-title Current thread title, or thread identifier when unnamed
[ ] model Current model name
[ ] model-with-reasoning Current model name with reasoning level
[ ] current-dir Current working directory
[ ] reasoning Current reasoning level
my-project · feature/mixed-preview · Mixed preview thread
Press space to toggle; ←/→ to move; enter to confirm and close; esc to close

View File

@@ -13,8 +13,8 @@ expression: status_line_popup_snapshot(&mut chat)
[x] weekly-limit Remaining usage on the weekly usage limit (omitted when unavailable)
[ ] model Current model name
[ ] model-with-reasoning Current model name with reasoning level
[ ] reasoning Current reasoning level
[ ] current-dir Current working directory
[ ] project-name Project name (omitted when unavailable)
monthly 65% left · weekly 50% left
Press space to toggle; ←/→ to move; enter to confirm and close; esc to close

View File

@@ -562,6 +562,7 @@ impl ChatWidget {
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().to_string()),
StatusLineItem::CurrentDir => {
Some(format_directory_display(
self.status_line_cwd(),
@@ -694,6 +695,7 @@ impl ChatWidget {
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)
}
@@ -759,12 +761,20 @@ impl ChatWidget {
self.model_with_reasoning_display_name(),
/*max_chars*/ 32,
)),
TerminalTitleItem::Reasoning => Some(Self::truncate_terminal_title_part(
self.reasoning_display_name().to_string(),
/*max_chars*/ 32,
)),
TerminalTitleItem::TaskProgress => self.terminal_title_task_progress(),
}
}
fn reasoning_display_name(&self) -> &'static str {
Self::status_line_reasoning_effort_label(self.effective_reasoning_effort())
}
fn model_with_reasoning_display_name(&self) -> String {
let label = Self::status_line_reasoning_effort_label(self.effective_reasoning_effort());
let label = self.reasoning_display_name();
let service_tier_label = self
.current_service_tier()
.and_then(|service_tier| {

View File

@@ -2208,6 +2208,37 @@ async fn terminal_title_model_updates_on_model_change_without_manual_refresh() {
assert_eq!(chat.last_terminal_title, Some("gpt-5.3-codex".to_string()));
}
#[tokio::test]
async fn status_line_and_terminal_title_reasoning_render_only_effort() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.4")).await;
chat.config.tui_status_line = Some(vec!["reasoning".to_string()]);
chat.config.tui_terminal_title = Some(vec!["reasoning".to_string()]);
chat.set_reasoning_effort(Some(ReasoningEffortConfig::XHigh));
chat.set_service_tier(Some(ServiceTier::Fast.request_value().to_string()));
chat.refresh_status_line();
chat.refresh_terminal_title();
assert_eq!(status_line_text(&chat), Some("xhigh".to_string()));
assert_eq!(chat.last_terminal_title, Some("xhigh".to_string()));
}
#[tokio::test]
async fn status_line_reasoning_updates_on_mode_switch_without_manual_refresh() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.3-codex")).await;
chat.set_feature_enabled(Feature::CollaborationModes, /*enabled*/ true);
chat.config.tui_status_line = Some(vec!["reasoning".to_string()]);
chat.set_reasoning_effort(Some(ReasoningEffortConfig::High));
assert_eq!(status_line_text(&chat), Some("high".to_string()));
let plan_mask = collaboration_modes::plan_mask(chat.model_catalog.as_ref())
.expect("expected plan collaboration mode");
chat.set_collaboration_mask(plan_mask);
assert_eq!(status_line_text(&chat), Some("medium".to_string()));
}
#[tokio::test]
async fn status_line_model_with_reasoning_updates_on_mode_switch_without_manual_refresh() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.3-codex")).await;