Skip to content

Commit b6d43ec

Browse files
authored
feat: status line with real data (openai#13619)
1 parent 98dca99 commit b6d43ec

File tree

4 files changed

+177
-37
lines changed

4 files changed

+177
-37
lines changed

codex-rs/tui/src/bottom_pane/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ pub(crate) use feedback_view::feedback_upload_consent_params;
9494
pub(crate) use skills_toggle_view::SkillsToggleItem;
9595
pub(crate) use skills_toggle_view::SkillsToggleView;
9696
pub(crate) use status_line_setup::StatusLineItem;
97+
pub(crate) use status_line_setup::StatusLinePreviewData;
9798
pub(crate) use status_line_setup::StatusLineSetupView;
9899
mod paste_burst;
99100
mod pending_input_preview;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
source: tui/src/bottom_pane/status_line_setup.rs
3+
assertion_line: 365
4+
expression: "render_lines(&view, 72)"
5+
---
6+
7+
Configure Status Line
8+
Select which items to display in the status line.
9+
10+
Type to search
11+
>
12+
› [x] model-name Current model name
13+
[x] current-dir Current working directory
14+
[x] git-branch Current Git branch (omitted when unavaila
15+
[ ] model-with-reasoning Current model name with reasoning level
16+
[ ] project-root Project root directory (omitted when unav
17+
[ ] context-remaining Percentage of context window remaining (o
18+
[ ] context-used Percentage of context window used (omitte
19+
[ ] five-hour-limit Remaining usage on 5-hour usage limit (om
20+
21+
gpt-5-codex · ~/codex-rs · jif/statusline-preview
22+
Use ↑↓ to navigate, ←→ to move, space to select, enter to confirm, esc

codex-rs/tui/src/bottom_pane/status_line_setup.rs

Lines changed: 149 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use ratatui::buffer::Buffer;
2121
use ratatui::layout::Rect;
2222
use ratatui::text::Line;
23+
use std::collections::BTreeMap;
2324
use std::collections::HashSet;
2425
use strum::IntoEnumIterator;
2526
use strum_macros::Display;
@@ -44,7 +45,7 @@ use crate::render::renderable::Renderable;
4445
/// - Git-related items only show when in a git repository
4546
/// - Context/limit items only show when data is available from the API
4647
/// - Session ID only shows after a session has started
47-
#[derive(EnumIter, EnumString, Display, Debug, Clone, Eq, PartialEq)]
48+
#[derive(EnumIter, EnumString, Display, Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
4849
#[strum(serialize_all = "kebab_case")]
4950
pub(crate) enum StatusLineItem {
5051
/// The current model name.
@@ -126,28 +127,36 @@ impl StatusLineItem {
126127
}
127128
}
128129
}
130+
}
129131

130-
/// Returns an example rendering of this item for the preview.
131-
///
132-
/// These are placeholder values used to show users what each item looks
133-
/// like in the status line before they confirm their selection.
134-
pub(crate) fn render(&self) -> &'static str {
135-
match self {
136-
StatusLineItem::ModelName => "gpt-5.2-codex",
137-
StatusLineItem::ModelWithReasoning => "gpt-5.2-codex medium",
138-
StatusLineItem::CurrentDir => "~/project/path",
139-
StatusLineItem::ProjectRoot => "~/project",
140-
StatusLineItem::GitBranch => "feat/awesome-feature",
141-
StatusLineItem::ContextRemaining => "18% left",
142-
StatusLineItem::ContextUsed => "82% used",
143-
StatusLineItem::FiveHourLimit => "5h 100%",
144-
StatusLineItem::WeeklyLimit => "weekly 98%",
145-
StatusLineItem::CodexVersion => "v0.93.0",
146-
StatusLineItem::ContextWindowSize => "258K window",
147-
StatusLineItem::UsedTokens => "27.3K used",
148-
StatusLineItem::TotalInputTokens => "17,588 in",
149-
StatusLineItem::TotalOutputTokens => "265 out",
150-
StatusLineItem::SessionId => "019c19bd-ceb6-73b0-adc8-8ec0397b85cf",
132+
/// Runtime values used to preview the current status-line selection.
133+
#[derive(Clone, Debug, Default, Eq, PartialEq)]
134+
pub(crate) struct StatusLinePreviewData {
135+
values: BTreeMap<StatusLineItem, String>,
136+
}
137+
138+
impl StatusLinePreviewData {
139+
pub(crate) fn from_iter<I>(values: I) -> Self
140+
where
141+
I: IntoIterator<Item = (StatusLineItem, String)>,
142+
{
143+
Self {
144+
values: values.into_iter().collect(),
145+
}
146+
}
147+
148+
fn line_for_items(&self, items: &[MultiSelectItem]) -> Option<Line<'static>> {
149+
let preview = items
150+
.iter()
151+
.filter(|item| item.enabled)
152+
.filter_map(|item| item.id.parse::<StatusLineItem>().ok())
153+
.filter_map(|item| self.values.get(&item).cloned())
154+
.collect::<Vec<_>>()
155+
.join(" · ");
156+
if preview.is_empty() {
157+
None
158+
} else {
159+
Some(Line::from(preview))
151160
}
152161
}
153162
}
@@ -175,7 +184,11 @@ impl StatusLineSetupView {
175184
///
176185
/// Items from `status_line_items` are shown first (in order) and marked as
177186
/// enabled. Remaining items are appended and marked as disabled.
178-
pub(crate) fn new(status_line_items: Option<&[String]>, app_event_tx: AppEventSender) -> Self {
187+
pub(crate) fn new(
188+
status_line_items: Option<&[String]>,
189+
preview_data: StatusLinePreviewData,
190+
app_event_tx: AppEventSender,
191+
) -> Self {
179192
let mut used_ids = HashSet::new();
180193
let mut items = Vec::new();
181194

@@ -212,20 +225,7 @@ impl StatusLineSetupView {
212225
])
213226
.items(items)
214227
.enable_ordering()
215-
.on_preview(|items| {
216-
let preview = items
217-
.iter()
218-
.filter(|item| item.enabled)
219-
.filter_map(|item| item.id.parse::<StatusLineItem>().ok())
220-
.map(|item| item.render())
221-
.collect::<Vec<_>>()
222-
.join(" · ");
223-
if preview.is_empty() {
224-
None
225-
} else {
226-
Some(Line::from(preview))
227-
}
228-
})
228+
.on_preview(move |items| preview_data.line_for_items(items))
229229
.on_confirm(|ids, app_event| {
230230
let items = ids
231231
.iter()
@@ -276,3 +276,115 @@ impl Renderable for StatusLineSetupView {
276276
self.picker.desired_height(width)
277277
}
278278
}
279+
280+
#[cfg(test)]
281+
mod tests {
282+
use super::*;
283+
use crate::app_event_sender::AppEventSender;
284+
use insta::assert_snapshot;
285+
use pretty_assertions::assert_eq;
286+
use ratatui::buffer::Buffer;
287+
use ratatui::layout::Rect;
288+
use tokio::sync::mpsc::unbounded_channel;
289+
290+
use crate::app_event::AppEvent;
291+
292+
#[test]
293+
fn preview_uses_runtime_values() {
294+
let preview_data = StatusLinePreviewData::from_iter([
295+
(StatusLineItem::ModelName, "gpt-5".to_string()),
296+
(StatusLineItem::CurrentDir, "/repo".to_string()),
297+
]);
298+
let items = vec![
299+
MultiSelectItem {
300+
id: StatusLineItem::ModelName.to_string(),
301+
name: String::new(),
302+
description: None,
303+
enabled: true,
304+
},
305+
MultiSelectItem {
306+
id: StatusLineItem::CurrentDir.to_string(),
307+
name: String::new(),
308+
description: None,
309+
enabled: true,
310+
},
311+
];
312+
313+
assert_eq!(
314+
preview_data.line_for_items(&items),
315+
Some(Line::from("gpt-5 · /repo"))
316+
);
317+
}
318+
319+
#[test]
320+
fn preview_omits_items_without_runtime_values() {
321+
let preview_data =
322+
StatusLinePreviewData::from_iter([(StatusLineItem::ModelName, "gpt-5".to_string())]);
323+
let items = vec![
324+
MultiSelectItem {
325+
id: StatusLineItem::ModelName.to_string(),
326+
name: String::new(),
327+
description: None,
328+
enabled: true,
329+
},
330+
MultiSelectItem {
331+
id: StatusLineItem::GitBranch.to_string(),
332+
name: String::new(),
333+
description: None,
334+
enabled: true,
335+
},
336+
];
337+
338+
assert_eq!(
339+
preview_data.line_for_items(&items),
340+
Some(Line::from("gpt-5"))
341+
);
342+
}
343+
344+
#[test]
345+
fn setup_view_snapshot_uses_runtime_preview_values() {
346+
let (tx_raw, _rx) = unbounded_channel::<AppEvent>();
347+
let view = StatusLineSetupView::new(
348+
Some(&[
349+
StatusLineItem::ModelName.to_string(),
350+
StatusLineItem::CurrentDir.to_string(),
351+
StatusLineItem::GitBranch.to_string(),
352+
]),
353+
StatusLinePreviewData::from_iter([
354+
(StatusLineItem::ModelName, "gpt-5-codex".to_string()),
355+
(StatusLineItem::CurrentDir, "~/codex-rs".to_string()),
356+
(
357+
StatusLineItem::GitBranch,
358+
"jif/statusline-preview".to_string(),
359+
),
360+
(StatusLineItem::WeeklyLimit, "weekly 82%".to_string()),
361+
]),
362+
AppEventSender::new(tx_raw),
363+
);
364+
365+
assert_snapshot!(render_lines(&view, 72));
366+
}
367+
368+
fn render_lines(view: &StatusLineSetupView, width: u16) -> String {
369+
let height = view.desired_height(width);
370+
let area = Rect::new(0, 0, width, height);
371+
let mut buf = Buffer::empty(area);
372+
view.render(area, &mut buf);
373+
374+
(0..area.height)
375+
.map(|row| {
376+
let mut line = String::new();
377+
for col in 0..area.width {
378+
let symbol = buf[(area.x + col, area.y + row)].symbol();
379+
if symbol.is_empty() {
380+
line.push(' ');
381+
} else {
382+
line.push_str(symbol);
383+
}
384+
}
385+
line
386+
})
387+
.collect::<Vec<_>>()
388+
.join("\n")
389+
}
390+
}

codex-rs/tui/src/chatwidget.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ use crate::app_event::RealtimeAudioDeviceKind;
4242
#[cfg(all(not(target_os = "linux"), feature = "voice-input"))]
4343
use crate::audio_device::list_realtime_audio_device_names;
4444
use crate::bottom_pane::StatusLineItem;
45+
use crate::bottom_pane::StatusLinePreviewData;
4546
use crate::bottom_pane::StatusLineSetupView;
4647
use crate::status::RateLimitWindowDisplay;
4748
use crate::status::format_directory_display;
@@ -5135,6 +5136,10 @@ impl ChatWidget {
51355136
let configured_status_line_items = self.configured_status_line_items();
51365137
let view = StatusLineSetupView::new(
51375138
Some(configured_status_line_items.as_slice()),
5139+
StatusLinePreviewData::from_iter(StatusLineItem::iter().filter_map(|item| {
5140+
self.status_line_value_for_item(&item)
5141+
.map(|value| (item, value))
5142+
})),
51385143
self.app_event_tx.clone(),
51395144
);
51405145
self.bottom_pane.show_view(Box::new(view));

0 commit comments

Comments
 (0)