Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
f9f40a5
feat(tui): add custom status line support
fcoury Jan 27, 2026
47e5151
feat(tui): wire up status line display and auto-refresh
fcoury Jan 27, 2026
7216abe
feat(tui): enhance status line with diff cost tracking and auto-refresh
fcoury Jan 27, 2026
d3fa4bf
feat(tui): add configurable timeout for status line commands
fcoury Jan 27, 2026
3373127
fix(tui): correct status line footer layout and percentage calculation
fcoury Jan 28, 2026
65553e9
fix(tui): improve status line footer layout with intelligent truncation
fcoury Jan 29, 2026
bc153d6
feat(tui): preserve collaboration mode indicator with a 1 char gap
fcoury Jan 29, 2026
fd80301
feat(tui): emit error on first status line command failure
fcoury Jan 29, 2026
24f5ad8
fix(tui): set project's current dir before executing status line command
fcoury Jan 29, 2026
695357e
fix(tui): kill the status line process when it times out
fcoury Jan 29, 2026
d741410
chore(tui): cargo fmt
fcoury Jan 29, 2026
4267e7c
fix(tui): shutdown stdin after writing to status line process
fcoury Jan 30, 2026
938f764
chore(tui): cleanup status line code and fix tests
fcoury Jan 30, 2026
b90d79f
fix(tui): use to_string_lossy to avoid panic with emoji on path
fcoury Jan 30, 2026
05901b3
fix(tui): clippy warnings
fcoury Jan 30, 2026
2608920
fix(tui): Code mode no longer displayed
fcoury Jan 30, 2026
8e588b1
fix(tui): clippy fixes
fcoury Jan 30, 2026
5089e3d
chore(tui): cargo fmt
fcoury Jan 30, 2026
5c22fe7
test(tui): warning on failure test did't read from stdin
fcoury Jan 31, 2026
a23dffd
feat(tui): track status_line metric when app starts with it enabled
fcoury Jan 31, 2026
8cab23a
feat(tui): context window percentage and 5h/7d limits
fcoury Feb 2, 2026
787edf4
feat(tui): add resets_at_raw to rate limit info
fcoury Feb 2, 2026
6328aff
feat(tui): send resets_at_raw with status line payload
fcoury Feb 2, 2026
f45a111
docs(tui): status_line module description
fcoury Feb 2, 2026
59bbcd3
feat(tui): throttle next execution if run takes less than 300ms
fcoury Feb 2, 2026
4ea192a
feat(tui): hardcoded timeout for status line execution at 500ms
fcoury Feb 2, 2026
67e9707
test(tui): guard status_line tests with cfg(unix)
fcoury Feb 2, 2026
14900e0
feat(tui): add /statusline command with multi-select picker UI
fcoury Feb 1, 2026
7964dc6
feat(tui): add live preview to /statusline configuration
fcoury Feb 1, 2026
723de8a
refactor(tui): remove status line live preview and simplify picker API
fcoury Feb 1, 2026
21370a2
refact(tui): replace external status line commands with built-in rend…
fcoury Feb 3, 2026
df01408
refact(tui): removed StatusLineValue
fcoury Feb 3, 2026
12ddc11
fix(tui): truncation consistency for status line
fcoury Feb 3, 2026
62a3e28
fix(tui): triggers on_cancel and message with proper enter behavior
fcoury Feb 3, 2026
50564b2
fix(tui): avoid double triggering of StatuLineSetupCancelled
fcoury Feb 3, 2026
1e28cee
feat(tui): add used tokens option to the status line
fcoury Feb 3, 2026
a4de03f
feat(tui): removed GitLines since it was misleading
fcoury Feb 3, 2026
ebc0691
fix(tui): update in-memory config after status line setup
fcoury Feb 3, 2026
a334731
fix(tui): prevent redundant git branch lookups in status line
fcoury Feb 3, 2026
93073bd
feat(tui): detects and warns user about invalid status line items
fcoury Feb 3, 2026
aa3dd79
fix(tui): git-branch lookup state when GitBranch is disabled/re-enabled
fcoury Feb 3, 2026
5e97e20
feat(tui): omit used tokens when 0 and clarify when items are omitted
fcoury Feb 3, 2026
06ef6e4
fix(tui): only apply status line setup on config save
fcoury Feb 3, 2026
bf9c601
refactor(tui): simplify status line setup selection
fcoury Feb 3, 2026
ac2f203
refactor(tui): reuse footer render helper
fcoury Feb 3, 2026
95931f5
fix(tui): treat missing status line as disabled
fcoury Feb 3, 2026
beacf20
fix(tui): model-with-reasoning and context-used consistent preview
fcoury Feb 3, 2026
913c591
refactor(tui): drop unused line counters
fcoury Feb 3, 2026
d2403ae
fix(tui): ellipsize status line preview
fcoury Feb 3, 2026
50a2be8
docs(tui): add documentation to multi-select picker and status line s…
fcoury Feb 3, 2026
ff76021
fix(tui): derive status line limit labels
fcoury Feb 3, 2026
b1a7f6b
fix(tui): typo 5-day to 5-hour window
fcoury Feb 3, 2026
8b855c1
fix(tui): close multiselect picker on cancel
fcoury Feb 3, 2026
e31fea3
fix(tui): gate footer status line on enable flag
fcoury Feb 3, 2026
d533e6b
fix(tui): preserve line style when truncating
fcoury Feb 4, 2026
04a8616
fix(tui): refresh branch names after each turn end
fcoury Feb 4, 2026
742d696
fix(tui): use close when handling Esc on MultiSelectPicker
fcoury Feb 4, 2026
fc0af6e
chore(tui): document status line related chatwidget members
fcoury Feb 4, 2026
3d7992e
fix(tui): clippy errors
fcoury Feb 4, 2026
623dfce
chore(tui): rename skill_name to name as match_item param
fcoury Feb 5, 2026
e76baee
chore(tui): keep WeeklyLimit display consistent with preview
fcoury Feb 5, 2026
ead5362
docs(tui): clarify status-line contracts and event semantics
joshka-oai Feb 5, 2026
234b68d
feat(tui): remove session prefix and add text suffixes where missing
fcoury Feb 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions codex-rs/core/config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -1046,6 +1046,14 @@
"default": true,
"description": "Show startup tooltips in the TUI welcome screen. Defaults to `true`.",
"type": "boolean"
},
"status_line": {
"default": null,
"description": "Ordered list of status line item identifiers.\n\nWhen set, the TUI renders the selected items as the status line.",
"items": {
"type": "string"
},
"type": "array"
}
},
"type": "object"
Expand Down
18 changes: 18 additions & 0 deletions codex-rs/core/src/config/edit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,24 @@ pub enum ConfigEdit {
ClearPath { segments: Vec<String> },
}

pub fn status_line_items_edit(items: &[String]) -> ConfigEdit {
if items.is_empty() {
return ConfigEdit::ClearPath {
segments: vec!["tui".to_string(), "status_line".to_string()],
};
}

let mut array = toml_edit::Array::new();
for item in items {
array.push(item.clone());
}

ConfigEdit::SetPath {
segments: vec!["tui".to_string(), "status_line".to_string()],
value: TomlItem::Value(array.into()),
}
}

// TODO(jif) move to a dedicated file
mod document_helpers {
use crate::config::types::McpServerConfig;
Expand Down
9 changes: 9 additions & 0 deletions codex-rs/core/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,9 @@ pub struct Config {
/// - `never`: Never use alternate screen (inline mode, preserves scrollback).
pub tui_alternate_screen: AltScreenMode,

/// Ordered list of status line item identifiers for the TUI.
pub tui_status_line: Option<Vec<String>>,

/// The directory that should be treated as the current working directory
/// for the session. All relative paths inside the business-logic layer are
/// resolved against this path.
Expand Down Expand Up @@ -1694,6 +1697,7 @@ impl Config {
.as_ref()
.map(|t| t.alternate_screen)
.unwrap_or_default(),
tui_status_line: cfg.tui.as_ref().and_then(|t| t.status_line.clone()),
otel: {
let t: OtelConfigToml = cfg.otel.unwrap_or_default();
let log_user_prompt = t.log_user_prompt.unwrap_or(false);
Expand Down Expand Up @@ -1931,6 +1935,7 @@ persistence = "none"
show_tooltips: true,
experimental_mode: None,
alternate_screen: AltScreenMode::Auto,
status_line: None,
}
);
}
Expand Down Expand Up @@ -3880,6 +3885,7 @@ model_verbosity = "high"
analytics_enabled: Some(true),
feedback_enabled: true,
tui_alternate_screen: AltScreenMode::Auto,
tui_status_line: None,
otel: OtelConfig::default(),
},
o3_profile_config
Expand Down Expand Up @@ -3965,6 +3971,7 @@ model_verbosity = "high"
analytics_enabled: Some(true),
feedback_enabled: true,
tui_alternate_screen: AltScreenMode::Auto,
tui_status_line: None,
otel: OtelConfig::default(),
};

Expand Down Expand Up @@ -4065,6 +4072,7 @@ model_verbosity = "high"
analytics_enabled: Some(false),
feedback_enabled: true,
tui_alternate_screen: AltScreenMode::Auto,
tui_status_line: None,
otel: OtelConfig::default(),
};

Expand Down Expand Up @@ -4151,6 +4159,7 @@ model_verbosity = "high"
analytics_enabled: Some(true),
feedback_enabled: true,
tui_alternate_screen: AltScreenMode::Auto,
tui_status_line: None,
otel: OtelConfig::default(),
};

Expand Down
6 changes: 6 additions & 0 deletions codex-rs/core/src/config/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,12 @@ pub struct Tui {
/// scrollback in terminal multiplexers like Zellij that follow the xterm spec.
#[serde(default)]
pub alternate_screen: AltScreenMode,

/// Ordered list of status line item identifiers.
///
/// When set, the TUI renders the selected items as the status line.
#[serde(default)]
pub status_line: Option<Vec<String>>,
}

const fn default_true() -> bool {
Expand Down
72 changes: 72 additions & 0 deletions codex-rs/tui/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,8 @@ pub(crate) struct App {

/// Controls the animation thread that sends CommitTick events.
pub(crate) commit_anim_running: Arc<AtomicBool>,
// Shared across ChatWidget instances so invalid status-line config warnings only emit once.
status_line_invalid_items_warned: Arc<AtomicBool>,

// Esc-backtracking state grouped
pub(crate) backtrack: crate::app_backtrack::BacktrackState,
Expand Down Expand Up @@ -605,6 +607,7 @@ impl App {
is_first_run: false,
feedback_audience: self.feedback_audience,
model: Some(self.chat_widget.current_model().to_string()),
status_line_invalid_items_warned: self.status_line_invalid_items_warned.clone(),
otel_manager: self.otel_manager.clone(),
}
}
Expand Down Expand Up @@ -906,6 +909,7 @@ impl App {
for event in snapshot.events {
self.handle_codex_event_replay(event);
}
self.refresh_status_line();
}

#[allow(clippy::too_many_arguments)]
Expand Down Expand Up @@ -982,6 +986,15 @@ impl App {
codex_core::terminal::user_agent(),
SessionSource::Cli,
);
if config
.tui_status_line
.as_ref()
.is_some_and(|cmd| !cmd.is_empty())
{
otel_manager.counter("codex.status_line", 1, &[]);
}

let status_line_invalid_items_warned = Arc::new(AtomicBool::new(false));

let enhanced_keys_supported = tui.enhanced_keys_supported();
let mut chat_widget = match session_selection {
Expand All @@ -1003,6 +1016,7 @@ impl App {
is_first_run,
feedback_audience,
model: Some(model.clone()),
status_line_invalid_items_warned: status_line_invalid_items_warned.clone(),
otel_manager: otel_manager.clone(),
};
ChatWidget::new(init, thread_manager.clone())
Expand Down Expand Up @@ -1032,6 +1046,7 @@ impl App {
is_first_run,
feedback_audience,
model: config.model.clone(),
status_line_invalid_items_warned: status_line_invalid_items_warned.clone(),
otel_manager: otel_manager.clone(),
};
ChatWidget::new_from_existing(init, resumed.thread, resumed.session_configured)
Expand Down Expand Up @@ -1061,6 +1076,7 @@ impl App {
is_first_run,
feedback_audience,
model: config.model.clone(),
status_line_invalid_items_warned: status_line_invalid_items_warned.clone(),
otel_manager: otel_manager.clone(),
};
ChatWidget::new_from_existing(init, forked.thread, forked.session_configured)
Expand Down Expand Up @@ -1092,6 +1108,7 @@ impl App {
deferred_history_lines: Vec::new(),
has_emitted_history_lines: false,
commit_anim_running: Arc::new(AtomicBool::new(false)),
status_line_invalid_items_warned: status_line_invalid_items_warned.clone(),
backtrack: BacktrackState::default(),
backtrack_render_pending: false,
feedback: feedback.clone(),
Expand Down Expand Up @@ -1220,6 +1237,13 @@ impl App {
tui: &mut tui::Tui,
event: TuiEvent,
) -> Result<AppRunControl> {
if matches!(event, TuiEvent::Draw) {
let size = tui.terminal.size()?;
if size != tui.terminal.last_known_screen_size {
self.refresh_status_line();
}
}

if self.overlay.is_some() {
let _ = self.handle_backtrack_overlay_event(tui, event).await?;
} else {
Expand Down Expand Up @@ -1293,6 +1317,7 @@ impl App {
is_first_run: false,
feedback_audience: self.feedback_audience,
model: Some(model),
status_line_invalid_items_warned: self.status_line_invalid_items_warned.clone(),
otel_manager: self.otel_manager.clone(),
};
self.chat_widget = ChatWidget::new(init, self.server.clone());
Expand Down Expand Up @@ -1562,12 +1587,15 @@ impl App {
}
AppEvent::UpdateReasoningEffort(effort) => {
self.on_update_reasoning_effort(effort);
self.refresh_status_line();
}
AppEvent::UpdateModel(model) => {
self.chat_widget.set_model(&model);
self.refresh_status_line();
}
AppEvent::UpdateCollaborationMode(mask) => {
self.chat_widget.set_collaboration_mask(mask);
self.refresh_status_line();
}
AppEvent::UpdatePersonality(personality) => {
self.on_update_personality(personality);
Expand Down Expand Up @@ -2201,11 +2229,45 @@ impl App {
));
}
},
AppEvent::StatusLineSetup { items } => {
let ids = items.iter().map(ToString::to_string).collect::<Vec<_>>();
let edit = codex_core::config::edit::status_line_items_edit(&ids);
let apply_result = ConfigEditsBuilder::new(&self.config.codex_home)
.with_edits([edit])
.apply()
.await;
match apply_result {
Ok(()) => {
self.config.tui_status_line = if ids.is_empty() {
None
} else {
Some(ids.clone())
};
self.chat_widget.setup_status_line(items);
}
Err(err) => {
tracing::error!(error = %err, "failed to persist status line items; keeping previous selection");
self.chat_widget
.add_error_message(format!("Failed to save status line items: {err}"));
}
}
}
AppEvent::StatusLineBranchUpdated { cwd, branch } => {
self.chat_widget.set_status_line_branch(cwd, branch);
self.refresh_status_line();
}
AppEvent::StatusLineSetupCancelled => {
self.chat_widget.cancel_status_line_setup();
}
}
Ok(AppRunControl::Continue)
}

fn handle_codex_event_now(&mut self, event: Event) {
let needs_refresh = matches!(
event.msg,
EventMsg::SessionConfigured(_) | EventMsg::TokenCount(_)
);
if self.suppress_shutdown_complete && matches!(event.msg, EventMsg::ShutdownComplete) {
self.suppress_shutdown_complete = false;
return;
Expand All @@ -2217,6 +2279,10 @@ impl App {
}
self.handle_backtrack_event(&event.msg);
self.chat_widget.handle_codex_event(event);

if needs_refresh {
self.refresh_status_line();
}
}

fn handle_codex_event_replay(&mut self, event: Event) {
Expand Down Expand Up @@ -2471,6 +2537,10 @@ impl App {
};
}

fn refresh_status_line(&mut self) {
self.chat_widget.refresh_status_line();
}

#[cfg(target_os = "windows")]
fn spawn_world_writable_scan(
cwd: PathBuf,
Expand Down Expand Up @@ -2627,6 +2697,7 @@ mod tests {
has_emitted_history_lines: false,
enhanced_keys_supported: false,
commit_anim_running: Arc::new(AtomicBool::new(false)),
status_line_invalid_items_warned: Arc::new(AtomicBool::new(false)),
backtrack: BacktrackState::default(),
backtrack_render_pending: false,
feedback: codex_feedback::CodexFeedback::new(),
Expand Down Expand Up @@ -2680,6 +2751,7 @@ mod tests {
has_emitted_history_lines: false,
enhanced_keys_supported: false,
commit_anim_running: Arc::new(AtomicBool::new(false)),
status_line_invalid_items_warned: Arc::new(AtomicBool::new(false)),
backtrack: BacktrackState::default(),
backtrack_render_pending: false,
feedback: codex_feedback::CodexFeedback::new(),
Expand Down
13 changes: 13 additions & 0 deletions codex-rs/tui/src/app_event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ use codex_protocol::ThreadId;
use codex_protocol::openai_models::ModelPreset;

use crate::bottom_pane::ApprovalRequest;
use crate::bottom_pane::StatusLineItem;
use crate::history_cell::HistoryCell;

use codex_core::features::Feature;
Expand Down Expand Up @@ -292,6 +293,18 @@ pub(crate) enum AppEvent {

/// Launch the external editor after a normal draw has completed.
LaunchExternalEditor,

/// Async update of the current git branch for status line rendering.
StatusLineBranchUpdated {
cwd: PathBuf,
branch: Option<String>,
},
/// Apply a user-confirmed status-line item ordering/selection.
StatusLineSetup {
items: Vec<StatusLineItem>,
},
/// Dismiss the status-line setup UI without changing config.
StatusLineSetupCancelled,
}

/// The exit strategy requested by the UI layer.
Expand Down
Loading
Loading