Honor the system clock preference across more TUI timestamps (#47321)

## Why

Several TUI views hard-code 24-hour timestamps even when the detected system preference is 12-hour time.

## What changed

Use the existing `ClockFormat` preference for rate-limit and spend-control reset times, usage notices, backend banners, reset-credit expiration times, analytics timestamps, and expanded session details in the resume picker. Capture the preference when constructing views and preserve existing timezone handling, including analytics' UTC labels.

## Testing

Add coverage for midnight, noon, and next-day reset labels in both clock formats, plus 12-hour analytics rendering with UTC labels. Update snapshots for status, banners, usage notices, reset-credit details, and expanded sessions, including narrow-width layouts. Pin test fixtures to explicit clock formats for deterministic output.

GitOrigin-RevId: 73c36d603ddc4eb7800614650a14c6f2961f8b70
This commit is contained in:
Eric Traut
2026-09-22 16:23:59 +00:00
committed by copyberry
parent 2c087a0cc2
commit 13a966fc65
36 changed files with 284 additions and 66 deletions
+2
View File
@@ -58,6 +58,7 @@ use sections::SectionState;
use sections::SectionStates;
pub(crate) struct AnalyticsView {
clock_format: crate::clock_format::ClockFormat,
model_names: std::collections::HashMap<String, String>,
sections: SectionStates,
chats: Load<chats::Chats>,
@@ -98,6 +99,7 @@ pub(crate) struct AnalyticsView {
impl AnalyticsView {
pub(crate) fn new(keymap: ListKeymap) -> Self {
let mut view = Self {
clock_format: crate::clock_format::ClockFormat::system(),
model_names: std::collections::HashMap::new(),
sections: SectionStates(std::array::from_fn(|_| SectionState::default())),
chats: Load::Unavailable,
+6 -3
View File
@@ -336,9 +336,12 @@ impl AnalyticsView {
.and_then(|value| chrono::DateTime::from_timestamp(value, /*nsecs*/ 0))
{
lines.push(
format!("Updated {} UTC", updated.format("%b %-d %H:%M"))
.set_style(secondary_style())
.into(),
format!(
"Updated {} UTC",
updated.format(self.clock_format.date_time_format())
)
.set_style(secondary_style())
.into(),
);
}
if side_by_side {
+1 -1
View File
@@ -160,7 +160,7 @@ impl AnalyticsView {
{
controls.push(format!(
"Report updated {} UTC",
updated.format("%b %-d %H:%M")
updated.format(self.clock_format.date_time_format())
));
}
controls
+6 -1
View File
@@ -277,7 +277,12 @@ impl AnalyticsView {
.and_then(|report| report.updated_at)
.and_then(|value| chrono::DateTime::from_timestamp(value, /*nsecs*/ 0));
let metadata = updated
.map(|updated| format!("Updated · {} UTC", updated.format("%b %-d %H:%M")))
.map(|updated| {
format!(
"Updated · {} UTC",
updated.format(self.clock_format.date_time_format())
)
})
.unwrap_or_default();
truncate(metadata.set_style(secondary_style()).into(), width).render(
Rect::new(area.x, area.bottom() - 1, area.width, /*height*/ 1),
+9 -6
View File
@@ -109,8 +109,8 @@ impl AnalyticsView {
let label = format!(
"{} {} – {}{}",
if selected { "›" } else { " " },
period.start.format("%b %-d %H:%M"),
period.end.format("%b %-d %H:%M"),
period.start.format(self.clock_format.date_time_format()),
period.end.format(self.clock_format.date_time_format()),
if period.start <= report.as_of && period.end > report.as_of {
" *"
} else {
@@ -227,16 +227,19 @@ impl AnalyticsView {
lines.push(
format!(
"Usage as of {} UTC · * current at last update",
as_of.format("%b %-d %H:%M")
as_of.format(self.clock_format.date_time_format())
)
.dim()
.into(),
);
if let Some(start) = coverage_start {
lines.push(
format!("Available since {} UTC", start.format("%b %-d %H:%M"))
.dim()
.into(),
format!(
"Available since {} UTC",
start.format(self.clock_format.date_time_format())
)
.dim()
.into(),
);
}
if *approximate {
+17
View File
@@ -343,3 +343,20 @@ fn invalid_history_returns_safe_errors() {
assert!(!error.contains("private wire value"));
}
}
#[test]
fn plan_clocks_follow_preference_and_keep_utc() {
let mut view = fixture::view(AccountKind::Consumer);
view.clock_format = crate::clock_format::ClockFormat::TwelveHour;
view.plan.enabled = true;
view.section = Section::Plan;
view.plan.report = Load::Ready(Report::parse(response()).unwrap().unwrap());
let rendered = view
.plan_lines(/*width*/ 80)
.0
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join("\n");
insta::assert_snapshot!("plan_clocks_twelve_hour", rendered);
}
@@ -15,6 +15,42 @@ fn report_controls_show_dates_and_update_time() {
assert!(output.contains("Updated · Sep 2 16:00 UTC"));
}
#[test]
fn twelve_hour_metadata_keeps_utc_across_renderers() {
let mut view = fixture::view(models::AccountKind::Business);
view.clock_format = crate::clock_format::ClockFormat::TwelveHour;
view.section = Section::Usage;
let controls = screen(&mut view, /*width*/ 40, /*height*/ 16);
view.zoomed = false;
view.tasks = Load::Ready(tasks::Chats {
rows: vec![tasks::Chat {
title: "Clock fixture".into(),
task: None,
}],
updated_at: Some("2026-09-02T16:00:00Z".parse().unwrap()),
..tasks::Chats::default()
});
let mut output = controls
.lines()
.filter(|line| line.contains("Updated"))
.map(str::to_owned)
.collect::<Vec<_>>();
for lines in [
view.history_lines(Section::Usage, /*width*/ 40, /*height*/ 16)
.lines,
view.help_lines(/*width*/ 40),
view.task_lines(/*width*/ 80).0,
] {
output.extend(
lines
.iter()
.map(ToString::to_string)
.filter(|line| line.contains("Updated") || line.contains("Report updated")),
);
}
insta::assert_snapshot!(output.join("\n"));
}
#[test]
fn tall_reports_use_available_height_and_wide_reports_put_details_beside_plot() {
for (width, height) in [(80, 60), (180, 60)] {
@@ -0,0 +1,15 @@
---
source: tui/src/analytics/plan_tests.rs
expression: rendered
---
› 5-hour limits
› Sep 2 10:00 AM – Sep 2 3:00 PM * 125%
Weekly limits
› Sep 1 12:00 AM – Sep 8 12:00 AM * Not available
Some periods aren't available yet
Usage as of Sep 2 12:00 PM UTC · * current at last update
Available since Sep 1 12:00 AM UTC
Amounts and period boundaries are approximate; recent activity may be delayed
@@ -0,0 +1,8 @@
---
source: tui/src/analytics/recovery_tests.rs
expression: "output.join(\"\\n\")"
---
" Updated · Sep 2 4:00 PM UTC "
Updated Sep 2 4:00 PM UTC
Report updated Sep 2 4:00 PM UTC
Updated Sep 2 4:00 PM UTC · recent activity may be delayed
+1 -1
View File
@@ -379,7 +379,7 @@ impl AnalyticsView {
coverage.push(
format!(
"Updated {} UTC · recent activity may be delayed",
time.format("%b %-d %H:%M")
time.format(self.clock_format.date_time_format())
)
.set_style(secondary_style())
.into(),
@@ -221,6 +221,7 @@ pub(super) fn view(kind: super::models::AccountKind) -> super::AnalyticsView {
/// Populate explicit report state after a test chooses its account, period, and grouping.
pub(super) fn seed_reports(view: &mut super::AnalyticsView) {
use super::data::Load;
view.clock_format = crate::clock_format::ClockFormat::TwentyFourHour;
if view.business() {
view.chats = Load::Ready(chats());
}
+7 -2
View File
@@ -7,16 +7,21 @@ use crate::app_event::AppEvent;
use crate::bottom_pane::ActionableBanner;
use crate::bottom_pane::BannerDismissal;
use crate::bottom_pane::SelectionItem;
use crate::clock_format::ClockFormat;
use chrono::DateTime;
use chrono::Local;
impl BackendBanner {
pub(crate) fn actionable_banner(&self) -> ActionableBanner {
pub(crate) fn actionable_banner(&self, clock_format: ClockFormat) -> ActionableBanner {
let reset_time = self
.reset_at
.and_then(|timestamp| DateTime::from_timestamp(timestamp, /*nsecs*/ 0))
.map(|time| {
crate::status::format_reset_timestamp(time.with_timezone(&Local), Local::now())
crate::status::format_reset_timestamp(
time.with_timezone(&Local),
Local::now(),
clock_format,
)
});
let copy = |text: &str| {
let text: String = text
+1 -1
View File
@@ -15,7 +15,7 @@ fn parse_rejects_more_actions_than_the_inline_banner_can_display() {
let banner = BackendBanner::parse(&raw).expect("eight actions fit");
assert_eq!(
banner
.actionable_banner()
.actionable_banner(crate::clock_format::ClockFormat::TwentyFourHour)
.actions
.into_iter()
.map(|action| action.name)
+3 -1
View File
@@ -2564,7 +2564,9 @@ mod tests {
.expect("valid optional banner");
let (tx, mut rx) = unbounded_channel();
let mut pane = test_pane(AppEventSender::new(tx));
pane.set_inline_banner(Some(banner.actionable_banner()));
pane.set_inline_banner(Some(
banner.actionable_banner(crate::clock_format::ClockFormat::TwentyFourHour),
));
let width = 44;
let area = Rect::new(
/*x*/ 0,
+1
View File
@@ -629,6 +629,7 @@ pub(crate) struct ChatWidget {
codex_rate_limit_reached_type: Option<RateLimitReachedType>,
codex_spend_control_reached: Option<bool>,
rate_limit_warnings: RateLimitWarningState,
clock_format: crate::clock_format::ClockFormat,
usage_notice_state: usage_notice::UsageNoticeState,
backend_banner_state: backend_banners::BackendBannerState,
automatic_model_switch_state: backend_banners::AutomaticModelSwitchState,
@@ -416,7 +416,7 @@ impl ChatWidget {
}
let is_reserve = banner.is_some_and(|banner| banner.banner_type == LUNA_RESERVE_BANNER);
let content = banner.map(|banner| {
let mut content = banner.actionable_banner();
let mut content = banner.actionable_banner(self.clock_format);
if banner.banner_type == LUNA_RESERVE_BANNER
&& self.current_model() != LUNA_RESERVE_MODEL
{
@@ -161,6 +161,7 @@ impl ChatWidget {
codex_rate_limit_reached_type: None,
codex_spend_control_reached: None,
rate_limit_warnings: RateLimitWarningState::default(),
clock_format: crate::clock_format::ClockFormat::system(),
usage_notice_state: usage_notice::UsageNoticeState::default(),
backend_banner_state: backend_banners::BackendBannerState::default(),
automatic_model_switch_state: backend_banners::AutomaticModelSwitchState::default(),
+6 -2
View File
@@ -378,8 +378,12 @@ impl ChatWidget {
.limit_name
.clone()
.unwrap_or_else(|| limit_id.clone());
let display =
rate_limit_snapshot_display_for_limit(&snapshot, limit_label, Local::now());
let display = rate_limit_snapshot_display_for_limit(
&snapshot,
limit_label,
Local::now(),
self.clock_format,
);
self.rate_limit_snapshots_by_limit_id
.insert(limit_id, display);
}
+6 -4
View File
@@ -1,3 +1,4 @@
use crate::clock_format::ClockFormat;
use chrono::DateTime;
use chrono::Local;
use chrono::Utc;
@@ -14,6 +15,7 @@ pub(super) struct ResetCreditOption {
pub(super) fn reset_credit_options(
summary: &RateLimitResetCreditsSummary,
clock_format: ClockFormat,
) -> Vec<ResetCreditOption> {
let available_count = summary.available_count.max(0);
let detail_limit = usize::try_from(available_count).unwrap_or(usize::MAX);
@@ -33,11 +35,11 @@ pub(super) fn reset_credit_options(
let expiration = match credit.expires_at {
Some(expires_at) => DateTime::<Utc>::from_timestamp(expires_at, 0)
.map(|expires_at| {
let expires_at = expires_at.with_timezone(&Local);
format!(
"Expires {}",
expires_at
.with_timezone(&Local)
.format("%H:%M on %-d %b %Y")
"Expires {} on {}",
expires_at.format(clock_format.time_format()),
expires_at.format("%-d %b %Y")
)
})
.unwrap_or_else(|| "Expiration unavailable".to_string()),
@@ -6,7 +6,7 @@ expression: "render_bottom_popup(&chat, 44)"
2 usage limit resets available.
› 1. Full reset Expires 09:39 on 18 Jun
› 1. Full reset Expires 9:39 AM on 18 Jun
2026.
2. Full reset Does not expire.
3. Cancel
@@ -2,8 +2,8 @@
source: tui/src/chatwidget/usage_notice_tests.rs
expression: "lines.join(\"\\n\")"
---
94 columns: ⚠ 5h limit: 8% left · resets at 13:00 · /status
93 columns: ⚠ 5h limit: 8% left · /status
98 columns: ⚠ 5h limit: 8% left · resets at 1:00 PM · /status
97 columns: ⚠ 5h limit: 8% left · /status
58 columns: ⚠ 5h limit: 8% left · /status
57 columns: ⚠ 5h limit: 8% left
38 columns: ⚠ 5h limit: 8% left
@@ -2,8 +2,8 @@
source: tui/src/chatwidget/usage_notice_tests.rs
expression: "lines.join(\"\\n\")"
---
132 columns: ⚠ weekly limit: only 2% left · resets at 12:00 on 21 Sep · /status
131 columns: ⚠ weekly limit: only 2% left · /status
138 columns: ⚠ weekly limit: only 2% left · resets at 12:00 PM on 21 Sep · /status
137 columns: ⚠ weekly limit: only 2% left · /status
76 columns: ⚠ weekly limit: only 2% left · /status
75 columns: ⚠ weekly limit: only 2% left
56 columns: ⚠ weekly limit: only 2% left
@@ -29,10 +29,25 @@ fn banner_response(
async fn backend_banner_presentation_and_cta_do_not_imply_recovery() {
for presentation in [None, Some("inline"), Some("dismissible")] {
let (mut chat, mut rx, _ops) = make_chatwidget_manual(Some("test-model-a")).await;
let response = banner_response(
let mut response = banner_response(
presentation,
json!([{"action":"notify_owner","label":"Notify owner"}]),
);
if presentation == Some("inline") {
use chrono::Timelike;
chat.clock_format = crate::clock_format::ClockFormat::TwelveHour;
let banner = response.rate_limit_upsell.as_mut().unwrap();
banner["description"] =
json!("Your usage resets at {time}. Ask your owner for credits.");
banner["reset_at"] = json!(
chrono::Local::now()
.with_hour(17)
.unwrap()
.with_minute(30)
.unwrap()
.timestamp()
);
}
chat.update_backend_banner(&response);
let initial = render_bottom_popup(&chat, /*width*/ 70);
assert!(initial.contains("Selected model usage exhausted"));
@@ -250,6 +250,7 @@ pub(super) async fn make_chatwidget_manual_with_auth(
session_telemetry,
};
let mut widget = ChatWidget::new_with_op_target(common, super::CodexOpTarget::Direct(op_tx));
widget.clock_format = crate::clock_format::ClockFormat::TwentyFourHour;
widget.windows_sandbox_host = crate::app::WindowsSandboxHost::Local;
widget.windows_sandbox_config.requirements = Some(None);
widget.transcript.active_cell = None;
@@ -3,7 +3,7 @@ source: tui/src/chatwidget/tests/backend_banners_tests.rs
expression: "normalize_snapshot_paths(render_bottom_popup(&chat, 70))"
---
Selected model usage exhausted
Ask your owner for credits or switch to another model.
Your usage resets at 5:30 PM. Ask your owner for credits.
› 1. Notify owner
+5 -4
View File
@@ -98,10 +98,10 @@ fn reset_credit_options_use_generic_copy_when_backend_copy_is_missing() {
credit.reset_type = RateLimitResetType::Unknown;
assert_eq!(
reset_credit_options(&detailed_reset_credits(
/*available_count*/ 1,
vec![credit],
)),
reset_credit_options(
&detailed_reset_credits(/*available_count*/ 1, vec![credit],),
crate::clock_format::ClockFormat::TwentyFourHour
),
vec![ResetCreditOption {
credit_id: Some("future-credit".to_string()),
name: "Full reset".to_string(),
@@ -416,6 +416,7 @@ async fn rate_limit_reset_popup_states_snapshot() {
#[tokio::test]
async fn rate_limit_reset_picker_wraps_expiry_details_snapshot() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
chat.clock_format = crate::clock_format::ClockFormat::TwelveHour;
let request_id = chat.show_rate_limit_reset_loading_popup();
let expiry = expiry_timestamp(/*day*/ 18, /*hour*/ 9, /*minute*/ 39);
assert!(chat.finish_rate_limit_reset_credits_refresh(
+1 -1
View File
@@ -165,7 +165,7 @@ impl ChatWidget {
reset_credits: &RateLimitResetCreditsSummary,
) -> SelectionViewParams {
let confirmation_gate = Arc::new(AtomicBool::new(true));
let mut items = reset_credit_options(reset_credits)
let mut items = reset_credit_options(reset_credits, self.clock_format)
.into_iter()
.map(|option| {
let confirmation_gate = confirmation_gate.clone();
+14 -3
View File
@@ -6,6 +6,7 @@ use super::ChatWidget;
use super::rate_limits::RateLimitSnapshotSource;
use super::rate_limits::is_approximate_window;
use super::rate_limits::limit_label_for_window;
use crate::clock_format::ClockFormat;
use crate::footer_hint::first_fitting_line;
use chrono::DateTime;
use chrono::Local;
@@ -27,7 +28,12 @@ pub(super) struct UsageNoticeState {
}
impl UsageNoticeState {
fn line(&self, width: u16, now: DateTime<Local>) -> Option<Line<'static>> {
fn line(
&self,
width: u16,
now: DateTime<Local>,
clock_format: ClockFormat,
) -> Option<Line<'static>> {
let (window, is_secondary) = self.current()?;
let label = limit_label_for_window(window.window_duration_mins, is_secondary);
// Percentages are rounded by the protocol; 100% alone does not prove a hard stop.
@@ -49,7 +55,11 @@ impl UsageNoticeState {
.map(|reset| {
format!(
" · resets at {}",
crate::status::format_reset_timestamp(reset.with_timezone(&Local), now)
crate::status::format_reset_timestamp(
reset.with_timezone(&Local),
now,
clock_format
)
)
})
.unwrap_or_default();
@@ -161,7 +171,8 @@ impl ChatWidget {
{
return None;
}
self.usage_notice_state.line(width, Local::now())
self.usage_notice_state
.line(width, Local::now(), self.clock_format)
}
pub(crate) fn start_usage_notice_read(&mut self, request_id: u64) {
@@ -38,7 +38,7 @@ fn notice_details_fit_without_truncating_the_warning() {
primary: Some(primary),
..Default::default()
},
&[94, 93, 58, 57, 38, 37, 12, 11, 0][..],
&[98, 97, 58, 57, 38, 37, 12, 11, 0][..],
),
(
"weekly_usage_notice_widths",
@@ -50,12 +50,12 @@ fn notice_details_fit_without_truncating_the_warning() {
}),
..Default::default()
},
&[132, 131, 76, 75, 56, 55, 16, 15][..],
&[138, 137, 76, 75, 56, 55, 16, 15][..],
),
] {
let mut lines = Vec::new();
for &width in widths {
let line = state.line(width, now);
let line = state.line(width, now, ClockFormat::TwelveHour);
assert!(
line.as_ref()
.is_none_or(|line| line.width() <= usize::from(width))
@@ -89,7 +89,7 @@ fn notice_shows_only_valid_future_reset_times() {
] {
state.primary.as_mut().unwrap().resets_at = reset;
assert_eq!(
state.line(/*width*/ 160, now),
state.line(/*width*/ 160, now, ClockFormat::TwentyFourHour),
Some(
Line::from(format!("⚠ 5h limit: 8% left{expected} · /status"))
.style(crate::style::warning_notice_style().bold())
@@ -114,7 +114,11 @@ fn notice_emphasis_tracks_utilization_without_claiming_a_hard_stop() {
};
let style = crate::style::warning_notice_style();
assert_eq!(
state.line(/*width*/ 80, notice_time()),
state.line(
/*width*/ 80,
notice_time(),
ClockFormat::TwentyFourHour
),
Some(
Line::from(format!("⚠ 5h limit: {remaining} left · /status")).style(if bold {
style.bold()
+8 -1
View File
@@ -1,5 +1,5 @@
//! Detects the TUI host's clock preference once. Unknown preferences preserve 12-hour output;
//! detection never changes the process locale or runs while rendering a history cell.
//! detection never changes the process locale. Views capture the preference for consistent rendering.
use std::sync::OnceLock;
@@ -15,6 +15,13 @@ impl ClockFormat {
*FORMAT.get_or_init(|| detect().unwrap_or(Self::TwelveHour))
}
pub(crate) fn date_time_format(self) -> &'static str {
match self {
Self::TwelveHour => "%b %-d %-I:%M %p",
Self::TwentyFourHour => "%b %-d %H:%M",
}
}
pub(crate) fn time_format(self) -> &'static str {
match self {
Self::TwelveHour => "%-I:%M %p",
+21 -4
View File
@@ -6,6 +6,7 @@ use std::sync::Arc;
use crate::app_server_session::AppServerSession;
use crate::clipboard_paste::normalize_pasted_search_query;
use crate::clock_format::ClockFormat;
use crate::color::blend;
use crate::color::is_light;
use crate::key_hint::KeyBindingListExt;
@@ -824,6 +825,7 @@ impl Drop for AltScreenGuard<'_> {
}
struct PickerState {
clock_format: ClockFormat,
use_theme_colors: bool,
// Resolve local filesystem membership once per cwd for each page-loading cycle.
local_cwd_matches: HashMap<PathBuf, bool>,
@@ -1023,6 +1025,7 @@ impl PickerState {
action: SessionPickerAction,
) -> Self {
Self {
clock_format: ClockFormat::system(),
use_theme_colors: true,
requester,
relative_time_reference: None,
@@ -3280,12 +3283,19 @@ fn render_expanded_session_details(
vec![
expanded_detail_line("Session:", &session, width),
expanded_time_detail_line("Created:", reference, row.created_at, width),
expanded_time_detail_line(
"Created:",
reference,
row.created_at,
width,
state.clock_format,
),
expanded_time_detail_line(
"Updated:",
reference,
row.updated_at.or(row.created_at),
width,
state.clock_format,
),
expanded_detail_line("Directory:", &directory, width),
expanded_detail_line("Branch:", &branch, width),
@@ -3430,6 +3440,7 @@ fn expanded_time_detail_line(
reference: DateTime<Utc>,
ts: Option<DateTime<Utc>>,
width: u16,
clock_format: ClockFormat,
) -> Line<'static> {
let Some(ts) = ts else {
return expanded_detail_line(label, "-", width);
@@ -3437,7 +3448,7 @@ fn expanded_time_detail_line(
let value = format!(
"{} · {}",
format_relative_time_long(reference, ts),
format_timestamp(ts)
format_timestamp(ts, clock_format)
);
expanded_detail_line(label, &value, width)
}
@@ -3492,8 +3503,12 @@ fn plural_time(value: i64, unit: &str) -> String {
}
}
fn format_timestamp(ts: DateTime<Utc>) -> String {
ts.format("%Y-%m-%d %H:%M:%S").to_string()
fn format_timestamp(ts: DateTime<Utc>, clock_format: ClockFormat) -> String {
ts.format(match clock_format {
ClockFormat::TwelveHour => "%Y-%m-%d %-I:%M:%S %p",
ClockFormat::TwentyFourHour => "%Y-%m-%d %H:%M:%S",
})
.to_string()
}
fn render_empty_state_line(state: &PickerState) -> Line<'static> {
@@ -4133,6 +4148,7 @@ mod tests {
/*filter_cwd*/ None,
SessionPickerAction::Resume,
);
state.clock_format = ClockFormat::TwentyFourHour;
state.relative_time_reference = parse_timestamp_str("2026-05-02T14:48:19Z");
let row = Row {
path: Some(PathBuf::from("/tmp/a.jsonl")),
@@ -5811,6 +5827,7 @@ session_picker_view = "dense"
/*filter_cwd*/ None,
SessionPickerAction::Resume,
);
state.clock_format = ClockFormat::TwelveHour;
state.all_rows = vec![row.clone()];
state.filtered_rows = vec![row];
state.relative_time_reference =
@@ -4,8 +4,8 @@ expression: rendered
---
⌄ Investigate picker expansion
│ Session: 019dabc1-0ef5-7431-b81c-03037f51f62c
│ Created: 1 hour ago · 2026-04-28 16:30:00
│ Updated: 15 minutes ago · 2026-04-28 17:45:00
│ Created: 1 hour ago · 2026-04-28 4:30:00 PM
│ Updated: 15 minutes ago · 2026-04-28 5:45:00 PM
│ Directory: /tmp/codex
│ Branch:  fcoury/session-picker
│
+7 -2
View File
@@ -1,3 +1,4 @@
use crate::clock_format::ClockFormat;
use crate::exec_command::relativize_to_home;
use crate::legacy_core::config::Config;
use crate::status::StatusAccountDisplay;
@@ -180,8 +181,12 @@ pub(crate) fn format_directory_display(directory: &Path, max_width: Option<usize
formatted
}
pub(crate) fn format_reset_timestamp(dt: DateTime<Local>, captured_at: DateTime<Local>) -> String {
let time = dt.format("%H:%M").to_string();
pub(crate) fn format_reset_timestamp(
dt: DateTime<Local>,
captured_at: DateTime<Local>,
clock_format: ClockFormat,
) -> String {
let time = dt.format(clock_format.time_format()).to_string();
if dt.date_naive() == captured_at.date_naive() {
time
} else {
+24 -11
View File
@@ -7,6 +7,7 @@
//! capture timestamp so stale detection and reset labels remain coherent for a given draw cycle.
use crate::chatwidget::fallback_limit_label;
use crate::chatwidget::limit_label_for_window;
use crate::clock_format::ClockFormat;
use crate::text_formatting::capitalize_first;
use super::helpers::format_reset_timestamp;
@@ -76,12 +77,17 @@ pub(crate) struct RateLimitWindowDisplay {
}
impl RateLimitWindowDisplay {
fn from_window(window: &RateLimitWindow, captured_at: DateTime<Local>) -> Self {
fn from_window(
window: &RateLimitWindow,
captured_at: DateTime<Local>,
clock_format: ClockFormat,
) -> Self {
let resets_at_utc = window
.resets_at
.and_then(|seconds| DateTime::<Utc>::from_timestamp(seconds, 0))
.map(|dt| dt.with_timezone(&Local));
let resets_at = resets_at_utc.map(|dt| format_reset_timestamp(dt, captured_at));
let resets_at =
resets_at_utc.map(|dt| format_reset_timestamp(dt, captured_at, clock_format));
Self {
used_percent: f64::from(window.used_percent),
@@ -140,13 +146,19 @@ pub(crate) fn rate_limit_snapshot_display(
snapshot: &RateLimitSnapshot,
captured_at: DateTime<Local>,
) -> RateLimitSnapshotDisplay {
rate_limit_snapshot_display_for_limit(snapshot, "codex".to_string(), captured_at)
rate_limit_snapshot_display_for_limit(
snapshot,
"codex".to_string(),
captured_at,
ClockFormat::TwentyFourHour,
)
}
pub(crate) fn rate_limit_snapshot_display_for_limit(
snapshot: &RateLimitSnapshot,
limit_name: String,
captured_at: DateTime<Local>,
clock_format: ClockFormat,
) -> RateLimitSnapshotDisplay {
RateLimitSnapshotDisplay {
normal_model_slug: snapshot.normal_model_slug.clone(),
@@ -155,16 +167,15 @@ pub(crate) fn rate_limit_snapshot_display_for_limit(
primary: snapshot
.primary
.as_ref()
.map(|window| RateLimitWindowDisplay::from_window(window, captured_at)),
.map(|window| RateLimitWindowDisplay::from_window(window, captured_at, clock_format)),
secondary: snapshot
.secondary
.as_ref()
.map(|window| RateLimitWindowDisplay::from_window(window, captured_at)),
.map(|window| RateLimitWindowDisplay::from_window(window, captured_at, clock_format)),
credits: snapshot.credits.as_ref().map(CreditsSnapshotDisplay::from),
individual_limit: snapshot
.individual_limit
.as_ref()
.and_then(|limit| SpendControlLimitSnapshotDisplay::from_limit(limit, captured_at)),
individual_limit: snapshot.individual_limit.as_ref().and_then(|limit| {
SpendControlLimitSnapshotDisplay::from_limit(limit, captured_at, clock_format)
}),
}
}
@@ -182,14 +193,16 @@ impl SpendControlLimitSnapshotDisplay {
fn from_limit(
value: &CoreSpendControlLimitSnapshot,
captured_at: DateTime<Local>,
clock_format: ClockFormat,
) -> Option<Self> {
Some(Self {
captured_at,
percent_remaining: f64::from(value.remaining_percent.clamp(0, 100)),
used: format_credit_amount(&value.used)?,
limit: format_credit_amount(&value.limit)?,
resets_at: DateTime::<Utc>::from_timestamp(value.resets_at, 0)
.map(|dt| format_reset_timestamp(dt.with_timezone(&Local), captured_at)),
resets_at: DateTime::<Utc>::from_timestamp(value.resets_at, 0).map(|dt| {
format_reset_timestamp(dt.with_timezone(&Local), captured_at, clock_format)
}),
})
}
}
@@ -17,6 +17,6 @@ expression: sanitized
│ │
│ Token usage: 1.9K total (1K input + 900 output) │
│ Context window: 100% left (2.25K used / 272K) │
│ 5h limit: [██████░░░░░░░░░░░░░░] 28% left (resets 03:14) │
│ Weekly limit: [███████████░░░░░░░░░] 55% left (resets 03:24) │
│ 5h limit: [██████░░░░░░░░░░░░░░] 28% left (resets 3:14 AM) │
│ Weekly limit: [███████████░░░░░░░░░] 55% left (resets 3:24 AM) │
╰───────────────────────────────────────────────────────────────────────────╯
+40 -1
View File
@@ -7,6 +7,7 @@ use super::rate_limits::RateLimitWindowDisplay;
use super::rate_limits::SpendControlLimitSnapshotDisplay;
use super::rate_limits::StatusRateLimitData;
use super::rate_limits::compose_rate_limit_data_many;
use crate::clock_format::ClockFormat;
use crate::history_cell::HistoryCell;
use crate::history_cell::PlainHistoryCell;
use crate::keymap::RuntimeKeymap;
@@ -329,7 +330,12 @@ async fn status_snapshot_includes_reasoning_details() {
plan_type: None,
rate_limit_reached_type: None,
};
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
let rate_display = super::rate_limits::rate_limit_snapshot_display_for_limit(
&snapshot,
"codex".to_string(),
captured_at,
ClockFormat::TwelveHour,
);
let model_slug = get_model_offline_for_tests(config.model.as_deref());
let token_info = token_info_for(&model_slug, &config, &usage);
@@ -2273,3 +2279,36 @@ async fn status_permissions_include_executor_profile_root() {
@r"Profile executor (workspace [\\server\share\foreign], Ask for approval)"
);
}
#[test]
fn reset_timestamps_follow_clock_preference() {
let captured_at = Local
.with_ymd_and_hms(
/*year*/ 2026, /*month*/ 9, /*day*/ 21, /*hour*/ 0, /*min*/ 0,
/*sec*/ 0,
)
.single()
.unwrap();
let labels = [ClockFormat::TwelveHour, ClockFormat::TwentyFourHour]
.into_iter()
.flat_map(|clock_format| {
[0, 12, 23, 24].map(|hours| {
super::helpers::format_reset_timestamp(
captured_at + ChronoDuration::hours(hours),
captured_at,
clock_format,
)
})
})
.collect::<Vec<_>>();
assert_snapshot!(labels.join("\n"), @"
12:00 AM
12:00 PM
11:00 PM
12:00 AM on 22 Sep
00:00
12:00
23:00
00:00 on 22 Sep
");
}