mirror of
https://github.com/openai/codex.git
synced 2026-04-29 10:53:24 +03:00
## Summary - Adds a new `/statusline` command to configure TUI footer status line - Introduces reusable `MultiSelectPicker` component with keyboard navigation, optional ordering and toggle support - Implement status line setup modal that persist configuration to config.toml ## Status Line Items The following items can be displayed in the status line: - **Model**: Current model name (with optional reasoning level) - **Context**: Remaining/used context window percentage - **Rate Limits**: 5-day and weekly usage limits - **Git**: Current branch (with optimized lookups) - **Tokens**: Used tokens, input/output token counts - **Session**: Session ID (full or shortened prefix) - **Paths**: Current directory, project root - **Version**: Codex version ## Features - Live preview while configuring status line items - Fuzzy search filtering in the picker - Intelligent truncation when items don't fit - Items gracefully omit when data is unavailable - Configuration persists to `config.toml` - Validates and warns about invalid status line items ## Test plan - [x] Run `/statusline` and verify picker UI appears - [x] Toggle items on/off and verify live preview updates - [x] Confirm selection persists after restart - [x] Verify truncation behavior with many items selected - [x] Test git branch detection in and out of git repos --------- Co-authored-by: Josh McKinney <joshka@openai.com>
279 lines
10 KiB
Rust
279 lines
10 KiB
Rust
//! Status line configuration view for customizing the TUI status bar.
|
|
//!
|
|
//! This module provides an interactive picker for selecting which items appear
|
|
//! in the status line at the bottom of the terminal. Users can:
|
|
//!
|
|
//! - **Select items**: Toggle which information is displayed
|
|
//! - **Reorder items**: Use left/right arrows to change display order
|
|
//! - **Preview changes**: See a live preview of the configured status line
|
|
//!
|
|
//! # Available Status Line Items
|
|
//!
|
|
//! - Model information (name, reasoning level)
|
|
//! - Directory paths (current dir, project root)
|
|
//! - Git information (branch name)
|
|
//! - Context usage (remaining %, used %, window size)
|
|
//! - Usage limits (5-hour, weekly)
|
|
//! - Session info (ID, tokens used)
|
|
//! - Application version
|
|
|
|
use ratatui::buffer::Buffer;
|
|
use ratatui::layout::Rect;
|
|
use ratatui::text::Line;
|
|
use std::collections::HashSet;
|
|
use strum::IntoEnumIterator;
|
|
use strum_macros::Display;
|
|
use strum_macros::EnumIter;
|
|
use strum_macros::EnumString;
|
|
|
|
use crate::app_event::AppEvent;
|
|
use crate::app_event_sender::AppEventSender;
|
|
use crate::bottom_pane::CancellationEvent;
|
|
use crate::bottom_pane::bottom_pane_view::BottomPaneView;
|
|
use crate::bottom_pane::multi_select_picker::MultiSelectItem;
|
|
use crate::bottom_pane::multi_select_picker::MultiSelectPicker;
|
|
use crate::render::renderable::Renderable;
|
|
|
|
/// Available items that can be displayed in the status line.
|
|
///
|
|
/// Each variant represents a piece of information that can be shown at the
|
|
/// bottom of the TUI. Items are serialized to kebab-case for configuration
|
|
/// storage (e.g., `ModelWithReasoning` becomes `model-with-reasoning`).
|
|
///
|
|
/// Some items are conditionally displayed based on availability:
|
|
/// - Git-related items only show when in a git repository
|
|
/// - Context/limit items only show when data is available from the API
|
|
/// - Session ID only shows after a session has started
|
|
#[derive(EnumIter, EnumString, Display, Debug, Clone, Eq, PartialEq)]
|
|
#[strum(serialize_all = "kebab_case")]
|
|
pub(crate) enum StatusLineItem {
|
|
/// The current model name.
|
|
ModelName,
|
|
|
|
/// Model name with reasoning level suffix.
|
|
ModelWithReasoning,
|
|
|
|
/// Current working directory path.
|
|
CurrentDir,
|
|
|
|
/// Project root directory (if detected).
|
|
ProjectRoot,
|
|
|
|
/// Current git branch name (if in a repository).
|
|
GitBranch,
|
|
|
|
/// Percentage of context window remaining.
|
|
ContextRemaining,
|
|
|
|
/// Percentage of context window used.
|
|
ContextUsed,
|
|
|
|
/// Remaining usage on the 5-hour rate limit.
|
|
FiveHourLimit,
|
|
|
|
/// Remaining usage on the weekly rate limit.
|
|
WeeklyLimit,
|
|
|
|
/// Codex application version.
|
|
CodexVersion,
|
|
|
|
/// Total context window size in tokens.
|
|
ContextWindowSize,
|
|
|
|
/// Total tokens used in the current session.
|
|
UsedTokens,
|
|
|
|
/// Total input tokens consumed.
|
|
TotalInputTokens,
|
|
|
|
/// Total output tokens generated.
|
|
TotalOutputTokens,
|
|
|
|
/// Full session UUID.
|
|
SessionId,
|
|
}
|
|
|
|
impl StatusLineItem {
|
|
/// User-visible description shown in the popup.
|
|
pub(crate) fn description(&self) -> &'static str {
|
|
match self {
|
|
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::GitBranch => "Current Git branch (omitted when unavailable)",
|
|
StatusLineItem::ContextRemaining => {
|
|
"Percentage of context window remaining (omitted when unknown)"
|
|
}
|
|
StatusLineItem::ContextUsed => {
|
|
"Percentage of context window used (omitted when unknown)"
|
|
}
|
|
StatusLineItem::FiveHourLimit => {
|
|
"Remaining usage on 5-hour usage limit (omitted when unavailable)"
|
|
}
|
|
StatusLineItem::WeeklyLimit => {
|
|
"Remaining usage on weekly usage limit (omitted when unavailable)"
|
|
}
|
|
StatusLineItem::CodexVersion => "Codex application version",
|
|
StatusLineItem::ContextWindowSize => {
|
|
"Total context window size in tokens (omitted when unknown)"
|
|
}
|
|
StatusLineItem::UsedTokens => "Total tokens used in session (omitted when zero)",
|
|
StatusLineItem::TotalInputTokens => "Total input tokens used in session",
|
|
StatusLineItem::TotalOutputTokens => "Total output tokens used in session",
|
|
StatusLineItem::SessionId => {
|
|
"Current session identifier (omitted until session starts)"
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Returns an example rendering of this item for the preview.
|
|
///
|
|
/// These are placeholder values used to show users what each item looks
|
|
/// like in the status line before they confirm their selection.
|
|
pub(crate) fn render(&self) -> &'static str {
|
|
match self {
|
|
StatusLineItem::ModelName => "gpt-5.2-codex",
|
|
StatusLineItem::ModelWithReasoning => "gpt-5.2-codex medium",
|
|
StatusLineItem::CurrentDir => "~/project/path",
|
|
StatusLineItem::ProjectRoot => "~/project",
|
|
StatusLineItem::GitBranch => "feat/awesome-feature",
|
|
StatusLineItem::ContextRemaining => "18% left",
|
|
StatusLineItem::ContextUsed => "82% used",
|
|
StatusLineItem::FiveHourLimit => "5h 100%",
|
|
StatusLineItem::WeeklyLimit => "weekly 98%",
|
|
StatusLineItem::CodexVersion => "v0.93.0",
|
|
StatusLineItem::ContextWindowSize => "258K window",
|
|
StatusLineItem::UsedTokens => "27.3K used",
|
|
StatusLineItem::TotalInputTokens => "17,588 in",
|
|
StatusLineItem::TotalOutputTokens => "265 out",
|
|
StatusLineItem::SessionId => "019c19bd-ceb6-73b0-adc8-8ec0397b85cf",
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Interactive view for configuring which items appear in the status line.
|
|
///
|
|
/// Wraps a [`MultiSelectPicker`] with status-line-specific behavior:
|
|
/// - Pre-populates items from current configuration
|
|
/// - Shows a live preview of the configured status line
|
|
/// - Emits [`AppEvent::StatusLineSetup`] on confirmation
|
|
/// - Emits [`AppEvent::StatusLineSetupCancelled`] on cancellation
|
|
pub(crate) struct StatusLineSetupView {
|
|
/// The underlying multi-select picker widget.
|
|
picker: MultiSelectPicker,
|
|
}
|
|
|
|
impl StatusLineSetupView {
|
|
/// Creates a new status line setup view.
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `status_line_items` - Currently configured item IDs (in display order),
|
|
/// or `None` to start with all items disabled
|
|
/// * `app_event_tx` - Event sender for dispatching configuration changes
|
|
///
|
|
/// Items from `status_line_items` are shown first (in order) and marked as
|
|
/// enabled. Remaining items are appended and marked as disabled.
|
|
pub(crate) fn new(status_line_items: Option<&[String]>, app_event_tx: AppEventSender) -> Self {
|
|
let mut used_ids = HashSet::new();
|
|
let mut items = Vec::new();
|
|
|
|
if let Some(selected_items) = status_line_items.as_ref() {
|
|
for id in *selected_items {
|
|
let Ok(item) = id.parse::<StatusLineItem>() else {
|
|
continue;
|
|
};
|
|
let item_id = item.to_string();
|
|
if !used_ids.insert(item_id.clone()) {
|
|
continue;
|
|
}
|
|
items.push(Self::status_line_select_item(item, true));
|
|
}
|
|
}
|
|
|
|
for item in StatusLineItem::iter() {
|
|
let item_id = item.to_string();
|
|
if used_ids.contains(&item_id) {
|
|
continue;
|
|
}
|
|
items.push(Self::status_line_select_item(item, false));
|
|
}
|
|
|
|
Self {
|
|
picker: MultiSelectPicker::builder(
|
|
"Configure Status Line".to_string(),
|
|
Some("Select which items to display in the status line.".to_string()),
|
|
app_event_tx,
|
|
)
|
|
.instructions(vec![
|
|
"Use ↑↓ to navigate, ←→ to move, space to select, enter to confirm, esc to cancel."
|
|
.into(),
|
|
])
|
|
.items(items)
|
|
.enable_ordering()
|
|
.on_preview(|items| {
|
|
let preview = items
|
|
.iter()
|
|
.filter(|item| item.enabled)
|
|
.filter_map(|item| item.id.parse::<StatusLineItem>().ok())
|
|
.map(|item| item.render())
|
|
.collect::<Vec<_>>()
|
|
.join(" · ");
|
|
if preview.is_empty() {
|
|
None
|
|
} else {
|
|
Some(Line::from(preview))
|
|
}
|
|
})
|
|
.on_confirm(|ids, app_event| {
|
|
let items = ids
|
|
.iter()
|
|
.map(|id| id.parse::<StatusLineItem>())
|
|
.collect::<Result<Vec<_>, _>>()
|
|
.unwrap_or_default();
|
|
app_event.send(AppEvent::StatusLineSetup { items });
|
|
})
|
|
.on_cancel(|app_event| {
|
|
app_event.send(AppEvent::StatusLineSetupCancelled);
|
|
})
|
|
.build(),
|
|
}
|
|
}
|
|
|
|
/// Converts a [`StatusLineItem`] into a [`MultiSelectItem`] for the picker.
|
|
fn status_line_select_item(item: StatusLineItem, enabled: bool) -> MultiSelectItem {
|
|
MultiSelectItem {
|
|
id: item.to_string(),
|
|
name: item.to_string(),
|
|
description: Some(item.description().to_string()),
|
|
enabled,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl BottomPaneView for StatusLineSetupView {
|
|
fn handle_key_event(&mut self, key_event: crossterm::event::KeyEvent) {
|
|
self.picker.handle_key_event(key_event);
|
|
}
|
|
|
|
fn is_complete(&self) -> bool {
|
|
self.picker.complete
|
|
}
|
|
|
|
fn on_ctrl_c(&mut self) -> CancellationEvent {
|
|
self.picker.close();
|
|
CancellationEvent::Handled
|
|
}
|
|
}
|
|
|
|
impl Renderable for StatusLineSetupView {
|
|
fn render(&self, area: Rect, buf: &mut Buffer) {
|
|
self.picker.render(area, buf)
|
|
}
|
|
|
|
fn desired_height(&self, width: u16) -> u16 {
|
|
self.picker.desired_height(width)
|
|
}
|
|
}
|