feat(tui): add /statusline command for interactive status line configuration (#10546)

## 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>
This commit is contained in:
Felipe Coury
2026-02-05 13:50:21 -03:00
committed by GitHub
parent 3b54fd7336
commit b0e5a6305b
25 changed files with 2324 additions and 83 deletions

View File

@@ -106,15 +106,20 @@ fn line_width(line: &Line<'_>) -> usize {
.sum()
}
fn truncate_line_to_width(line: Line<'static>, max_width: usize) -> Line<'static> {
pub(crate) fn truncate_line_to_width(line: Line<'static>, max_width: usize) -> Line<'static> {
if max_width == 0 {
return Line::from(Vec::<Span<'static>>::new());
}
let Line {
style,
alignment,
spans,
} = line;
let mut used = 0usize;
let mut spans_out: Vec<Span<'static>> = Vec::new();
for span in line.spans {
for span in spans {
let text = span.content.into_owned();
let style = span.style;
let span_width = UnicodeWidthStr::width(text.as_str());
@@ -151,10 +156,17 @@ fn truncate_line_to_width(line: Line<'static>, max_width: usize) -> Line<'static
break;
}
Line::from(spans_out)
Line {
style,
alignment,
spans: spans_out,
}
}
fn truncate_line_with_ellipsis_if_overflow(line: Line<'static>, max_width: usize) -> Line<'static> {
pub(crate) fn truncate_line_with_ellipsis_if_overflow(
line: Line<'static>,
max_width: usize,
) -> Line<'static> {
if max_width == 0 {
return Line::from(Vec::<Span<'static>>::new());
}
@@ -165,10 +177,18 @@ fn truncate_line_with_ellipsis_if_overflow(line: Line<'static>, max_width: usize
}
let truncated = truncate_line_to_width(line, max_width.saturating_sub(1));
let mut spans = truncated.spans;
let Line {
style,
alignment,
mut spans,
} = truncated;
let ellipsis_style = spans.last().map(|span| span.style).unwrap_or_default();
spans.push(Span::styled("", ellipsis_style));
Line::from(spans)
Line {
style,
alignment,
spans,
}
}
/// Computes the shared start column used for descriptions in selection rows.