mirror of
https://github.com/openai/codex.git
synced 2026-05-02 18:37:01 +00:00
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:
@@ -830,6 +830,7 @@ async fn helpers_are_available_and_do_not_panic() {
|
||||
is_first_run: true,
|
||||
feedback_audience: FeedbackAudience::External,
|
||||
model: Some(resolved_model),
|
||||
status_line_invalid_items_warned: Arc::new(AtomicBool::new(false)),
|
||||
otel_manager,
|
||||
};
|
||||
let mut w = ChatWidget::new(init, thread_manager);
|
||||
@@ -958,6 +959,12 @@ async fn make_chatwidget_manual(
|
||||
feedback: codex_feedback::CodexFeedback::new(),
|
||||
feedback_audience: FeedbackAudience::External,
|
||||
current_rollout_path: None,
|
||||
current_cwd: None,
|
||||
status_line_invalid_items_warned: Arc::new(AtomicBool::new(false)),
|
||||
status_line_branch: None,
|
||||
status_line_branch_cwd: None,
|
||||
status_line_branch_pending: false,
|
||||
status_line_branch_lookup_complete: false,
|
||||
external_editor_state: ExternalEditorState::Closed,
|
||||
};
|
||||
widget.set_model(&resolved_model);
|
||||
@@ -2637,6 +2644,7 @@ async fn collaboration_modes_defaults_to_code_on_startup() {
|
||||
is_first_run: true,
|
||||
feedback_audience: FeedbackAudience::External,
|
||||
model: Some(resolved_model.clone()),
|
||||
status_line_invalid_items_warned: Arc::new(AtomicBool::new(false)),
|
||||
otel_manager,
|
||||
};
|
||||
|
||||
@@ -2682,6 +2690,7 @@ async fn experimental_mode_plan_applies_on_startup() {
|
||||
is_first_run: true,
|
||||
feedback_audience: FeedbackAudience::External,
|
||||
model: Some(resolved_model.clone()),
|
||||
status_line_invalid_items_warned: Arc::new(AtomicBool::new(false)),
|
||||
otel_manager,
|
||||
};
|
||||
|
||||
@@ -4907,6 +4916,83 @@ async fn warning_event_adds_warning_history_cell() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn status_line_invalid_items_warn_once() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None).await;
|
||||
chat.config.tui_status_line = Some(vec![
|
||||
"model_name".to_string(),
|
||||
"bogus_item".to_string(),
|
||||
"lines_changed".to_string(),
|
||||
"bogus_item".to_string(),
|
||||
]);
|
||||
chat.thread_id = Some(ThreadId::new());
|
||||
|
||||
chat.refresh_status_line();
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
assert_eq!(cells.len(), 1, "expected one warning history cell");
|
||||
let rendered = lines_to_single_string(&cells[0]);
|
||||
assert!(
|
||||
rendered.contains("bogus_item"),
|
||||
"warning cell missing invalid item content: {rendered}"
|
||||
);
|
||||
|
||||
chat.refresh_status_line();
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
assert!(
|
||||
cells.is_empty(),
|
||||
"expected invalid status line warning to emit only once"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn status_line_branch_state_resets_when_git_branch_disabled() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None).await;
|
||||
chat.status_line_branch = Some("main".to_string());
|
||||
chat.status_line_branch_pending = true;
|
||||
chat.status_line_branch_lookup_complete = true;
|
||||
chat.config.tui_status_line = Some(vec!["model_name".to_string()]);
|
||||
|
||||
chat.refresh_status_line();
|
||||
|
||||
assert_eq!(chat.status_line_branch, None);
|
||||
assert!(!chat.status_line_branch_pending);
|
||||
assert!(!chat.status_line_branch_lookup_complete);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn status_line_branch_refreshes_after_turn_complete() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None).await;
|
||||
chat.config.tui_status_line = Some(vec!["git-branch".to_string()]);
|
||||
chat.status_line_branch_lookup_complete = true;
|
||||
chat.status_line_branch_pending = false;
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "turn-1".into(),
|
||||
msg: EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
last_agent_message: None,
|
||||
}),
|
||||
});
|
||||
|
||||
assert!(chat.status_line_branch_pending);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn status_line_branch_refreshes_after_interrupt() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None).await;
|
||||
chat.config.tui_status_line = Some(vec!["git-branch".to_string()]);
|
||||
chat.status_line_branch_lookup_complete = true;
|
||||
chat.status_line_branch_pending = false;
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "turn-1".into(),
|
||||
msg: EventMsg::TurnAborted(codex_core::protocol::TurnAbortedEvent {
|
||||
reason: TurnAbortReason::Interrupted,
|
||||
}),
|
||||
});
|
||||
|
||||
assert!(chat.status_line_branch_pending);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn stream_recovery_restores_previous_status_header() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None).await;
|
||||
|
||||
Reference in New Issue
Block a user