Expose experimental analytics plan history and improve navigation (#45772)

## What changed

- Expose `analytics_plan_history` in the experimental features menu for previewing consumer five-hour and weekly allowance history in `/analytics`. Keep it disabled by default.
- Add `h` and `l` as left/right navigation aliases in analytics, preserving explicit bindings and honoring remapped or unbound arrows.
- Use terminal-aware footer key colors to keep analytics shortcuts readable on light backgrounds.

## Testing

Add navigation tests for arrow equivalence, custom bindings, modifiers, and key repeat. Extend light-background contrast assertions and update style snapshots. Add a snapshot for the plan history experimental feature entry.

GitOrigin-RevId: 310e64a43a7dd08c8a3fd1efed6985805f7dd580
This commit is contained in:
Felipe Coury
2026-09-15 18:45:01 +00:00
committed by copyberry
parent de40696ec4
commit b1f3c2f77e
33 changed files with 450 additions and 60 deletions

View File

@@ -911,7 +911,11 @@ pub const FEATURES: &[FeatureSpec] = &[
FeatureSpec {
id: Feature::AnalyticsPlanHistory,
key: "analytics_plan_history",
stage: Stage::UnderDevelopment,
stage: Stage::Experimental {
name: "Analytics plan history",
menu_description: "Preview five-hour and weekly allowance history for consumer accounts in /analytics.",
announcement: "",
},
default_enabled: false,
},
FeatureSpec {

View File

@@ -297,7 +297,18 @@ impl AnalyticsView {
let action = if key_hint::plain(KeyCode::Char(' ')).is_press(key) {
Some(ListAction::Accept)
} else {
self.keymap.action_for(key)
self.keymap.action_for(key).or_else(|| {
if !key.modifiers.is_empty() {
return None;
}
// Keep explicit bindings first and honor remapped or unbound arrows.
let code = match key.code {
KeyCode::Char('h') => KeyCode::Left,
KeyCode::Char('l') => KeyCode::Right,
_ => return None,
};
self.keymap.action_for(KeyEvent { code, ..key })
})
};
self.follow_selection = true;

View File

@@ -0,0 +1,272 @@
//! Vim navigation aliases share arrow behavior while preserving configured bindings.
use super::*;
use crate::analytics::models::AccountKind;
use codex_config::types::KeybindingSpec;
use codex_config::types::KeybindingsSpec;
use codex_config::types::TuiKeymap;
use pretty_assertions::assert_eq;
use serde_json::json;
fn navigate(views: &mut [AnalyticsView; 2], keys: &[(char, KeyCode)], size: (u16, u16)) -> String {
let mut output = String::new();
for &(alias, arrow) in keys {
press(&mut views[0], KeyCode::Char(alias));
press(&mut views[1], arrow);
output = screen(&mut views[0], size.0, size.1);
assert_eq!(output, screen(&mut views[1], size.0, size.1), "{alias}");
}
output
}
#[test]
fn vim_chart_navigation_matches_arrows_in_focused_and_dashboard_views() {
for (kind, section) in [
(AccountKind::Consumer, Section::Usage),
(AccountKind::Consumer, Section::Activity),
(AccountKind::Consumer, Section::Plugins),
(AccountKind::Consumer, Section::Skills),
(AccountKind::Business, Section::Usage),
(AccountKind::Business, Section::Credits),
] {
for zoomed in [true, false] {
let mut views = [fixture::view(kind), fixture::view(kind)];
for view in &mut views {
view.section = section;
view.zoomed = zoomed;
view.sections[section].cursor = 3;
view.sections[section].detail = Some(3);
}
let output = navigate(
&mut views,
&[
('h', KeyCode::Left),
('h', KeyCode::Left),
('l', KeyCode::Right),
('j', KeyCode::Down),
('k', KeyCode::Up),
],
/*size*/ (96, 32),
);
assert_eq!(
(
views[0].section,
views[0].sections[section].cursor,
views[0].sections[section].detail
),
(section, 2, Some(2))
);
if kind == AccountKind::Consumer && section == Section::Usage && zoomed {
insta::assert_snapshot!("vim_chart_navigation", output);
}
}
}
}
#[test]
fn vim_plan_navigation_preserves_independent_windows_and_collapses_old_details() {
let mut views = [
fixture::view(AccountKind::Consumer),
fixture::view(AccountKind::Consumer),
];
for view in &mut views {
let as_of = "2026-09-02T12:00:00Z"
.parse::<chrono::DateTime<chrono::Utc>>()
.unwrap();
view.plan.enabled = true;
view.plan.report = Load::Ready(plan::Report {
as_of,
coverage_start: None,
coverage_complete: true,
approximate: false,
periods: std::array::from_fn(|window| {
let length = if window == 0 {
chrono::Duration::hours(/*hours*/ 5)
} else {
chrono::Duration::days(/*days*/ 7)
};
(0..2)
.map(|index| plan::Period {
id: format!("{window}-{index}"),
start: as_of - length * index,
end: as_of - length * index + length,
complete: true,
used: Some(2500.0),
breakdowns: None,
})
.collect()
}),
});
view.plan.poll();
view.plan.expanded = [Some("0-0".into()), Some("1-0".into())];
view.section = Section::Plan;
}
navigate(
&mut views,
&[
('l', KeyCode::Right),
('j', KeyCode::Down),
('k', KeyCode::Up),
('h', KeyCode::Left),
('j', KeyCode::Down),
],
/*size*/ (120, 30),
);
assert_eq!(
(
views[0].plan.window,
views[0].plan.cursor,
&views[0].plan.expanded
),
(0, [1, 0], &[None, None])
);
}
#[test]
fn vim_summary_navigation_scrolls_like_arrows() {
let mut views = [
fixture::view(AccountKind::Consumer),
fixture::view(AccountKind::Consumer),
];
for view in &mut views {
view.section = Section::Summary;
view.profile = Load::Ready(
serde_json::from_value(json!({
"stats": {"lifetime_tokens": 123, "daily_usage_buckets": [
{"start_date": "2026-09-02", "tokens": 123}
]}
}))
.unwrap(),
);
assert!(screen(view, /*width*/ 64, /*height*/ 18).contains("scroll"));
}
navigate(
&mut views,
&[
('h', KeyCode::Left),
('l', KeyCode::Right),
('j', KeyCode::Down),
('j', KeyCode::Down),
('k', KeyCode::Up),
],
/*size*/ (64, 18),
);
assert_eq!(views[0].scroll_offset, 1);
}
#[test]
fn vim_chat_navigation_expands_collapses_and_moves_like_arrows() {
for kind in [AccountKind::Consumer, AccountKind::Business] {
let mut views = [fixture::view(kind), fixture::view(kind)];
for view in &mut views {
view.section = Section::Chats;
if kind == AccountKind::Consumer {
view.tasks = Load::Ready(tasks::Chats {
rows: (0..2)
.map(|index| tasks::Chat {
title: format!("Example chat {index}"),
task: Some(
serde_json::from_value(json!({
"thread_id": format!("chat-{index}"),
"data_status": "available", "usage_source": "accounting",
"weekly_limit_percent": 20 - index,
"five_hour_limit_percent": null, "balance_usage_credits": null,
"groups": []
}))
.unwrap(),
),
})
.collect(),
..tasks::Chats::default()
});
}
}
navigate(
&mut views,
&[
('l', KeyCode::Right),
('h', KeyCode::Left),
('l', KeyCode::Right),
('j', KeyCode::Down),
('l', KeyCode::Right),
('k', KeyCode::Up),
],
/*size*/ (96, 32),
);
assert_eq!(
(
views[0].sections[Section::Chats].cursor,
views[0].sections[Section::Chats].detail
),
(0, None)
);
}
}
#[test]
fn configured_vim_keys_win_over_horizontal_aliases() {
let mut config = TuiKeymap::default();
config.list.jump_top = Some(KeybindingsSpec::Many(vec![
KeybindingSpec("h".into()),
KeybindingSpec("l".into()),
]));
let mut view = fixture::view(AccountKind::Consumer);
view.keymap = RuntimeKeymap::from_config(&config).unwrap().list;
for alias in ['h', 'l'] {
view.sections[Section::Usage].cursor = 3;
press(&mut view, KeyCode::Char(alias));
assert_eq!(view.sections[Section::Usage].cursor, 0);
}
}
#[test]
fn vim_aliases_respect_removed_and_remapped_arrows() {
let mut config = TuiKeymap::default();
config.list.move_left = Some(KeybindingsSpec::Many(Vec::new()));
config.list.move_right = Some(KeybindingsSpec::Many(Vec::new()));
config.list.move_up = Some(KeybindingsSpec::One(KeybindingSpec("up".into())));
config.list.move_down = Some(KeybindingsSpec::One(KeybindingSpec("down".into())));
let mut view = fixture::view(AccountKind::Consumer);
view.keymap = RuntimeKeymap::from_config(&config).unwrap().list;
view.sections[Section::Usage].cursor = 3;
for alias in ['h', 'j', 'k', 'l'] {
press(&mut view, KeyCode::Char(alias));
assert_eq!(view.sections[Section::Usage].cursor, 3);
}
config.list.move_up = Some(KeybindingsSpec::One(KeybindingSpec("right".into())));
config.list.move_down = Some(KeybindingsSpec::One(KeybindingSpec("left".into())));
view.keymap = RuntimeKeymap::from_config(&config).unwrap().list;
press(&mut view, KeyCode::Char('h'));
assert_eq!(view.sections[Section::Usage].cursor, 4);
press(&mut view, KeyCode::Char('l'));
assert_eq!(view.sections[Section::Usage].cursor, 3);
}
#[test]
fn horizontal_aliases_ignore_modifiers_and_release_but_accept_repeat() {
let mut view = fixture::view(AccountKind::Consumer);
view.sections[Section::Usage].cursor = 3;
for alias in ['h', 'l'] {
for modifiers in [
KeyModifiers::ALT,
KeyModifiers::SHIFT,
KeyModifiers::CONTROL | KeyModifiers::ALT,
] {
view.handle_key(KeyEvent::new(KeyCode::Char(alias), modifiers));
assert_eq!(view.sections[Section::Usage].cursor, 3);
}
view.handle_key(KeyEvent {
kind: KeyEventKind::Release,
..KeyEvent::new(KeyCode::Char(alias), KeyModifiers::NONE)
});
assert_eq!(view.sections[Section::Usage].cursor, 3);
}
for (alias, cursor) in [('h', 2), ('l', 3)] {
view.handle_key(KeyEvent {
kind: KeyEventKind::Repeat,
..KeyEvent::new(KeyCode::Char(alias), KeyModifiers::NONE)
});
assert_eq!(view.sections[Section::Usage].cursor, cursor);
}
}

View File

@@ -9,12 +9,14 @@ use crate::analytics::models::AccountAnalyticsValue;
use crate::analytics::sections::Section;
use crate::keymap::ListAction;
use crate::style::accent_style;
use crate::style::footer_hint_key_style;
use crate::wrapping::RtOptions;
use crate::wrapping::word_wrap_lines;
use ratatui::buffer::Buffer;
use ratatui::layout::Constraint;
use ratatui::layout::Layout;
use ratatui::layout::Rect;
use ratatui::style::Color;
use ratatui::style::Styled;
use ratatui::style::Stylize;
use ratatui::text::Line;
@@ -169,11 +171,11 @@ impl AnalyticsView {
spans.push(" · ".into());
}
let (keys, description) = control.rsplit_once(' ').unwrap_or((control, ""));
#[expect(
clippy::disallowed_methods,
reason = "Analytics shortcut keys intentionally use white to distinguish them from descriptions."
)]
spans.push(keys.to_owned().white().bold().not_dim());
spans.push(
keys.to_owned()
.fg(Color::Reset)
.patch_style(footer_hint_key_style().bold().not_dim()),
);
if !description.is_empty() {
spans.push(format!(" {description}").into());
}

View File

@@ -0,0 +1,37 @@
---
source: tui/src/analytics/navigation_tests.rs
assertion_line: 60
expression: output
---
" Analytics · Live account "
" [7d] 30d · Aug 27Sep 2, 2026 "
" "
" 1 Summary [2 Total usage history] 3 Messages 4 Plugins called 5 Skills used 6 Top chats "
" "
" ▎ Total usage history By feature "
" ──────────────────────────────────────────────────────────────────────────────────────────── "
" Total usage · relative units "
" Approximate · may be delayed up to 6 hours "
" 91 100 "
" 100 ▂▂▂▂▂▂▂▂ 81 ████████ 86 "
" 73 64 ████████ ▃▃▃▃▃▃▃▃ ████████ ▇▇▇▇▇▇▇▇ "
" ▆▆▆▆▆▆▆▆ ████████ ████████ ████████ ████████ "
" ████████ ████████ ████████ ████████ ████████ ████████ "
" 50 ████████ ████████ ████████ ████████ ████████ ████████ "
" 20 ████████ ████████ ████████ ████████ ████████ ████████ "
" ▄▄▄▄▄▄▄▄ ████████ ████████ ████████ ████████ ████████ ████████ "
" ████████ ████████ ████████ ████████ ████████ ████████ ████████ "
" 0 ─────────────────────────────────────────────────────────────────────────────────────── "
" ▲ "
" Aug 27 Aug 28 Aug 29 Aug 30 Aug 31 Sep 1 Sep 2 "
" "
" ──────────────────────────────────────────────────────── "
" Aug 29 · 64 usage units "
" Feature Share "
" ● Tasks 64.1% "
" ● Code review 14.1% "
" ● Images 10.9% "
" ● Other 10.9% "
" "
" tab/16 section · z dashboard · ←/→ day · enter details "
" r 7/30d · g group · R refresh · esc back · q close "

View File

@@ -1,5 +1,6 @@
---
source: tui/src/analytics/styles_tests.rs
assertion_line: 145
expression: snapshot
---
120x42
@@ -98,5 +99,5 @@ Rows (column:palette):
37: 0:0 2:6 4:0
38: 0:0 2:5 4:0
39: 0:0
40: 0:0 2:7 9:2 20:7 21:2 34:7 37:2 44:7 49:2 118:0
41: 0:0 2:7 3:2 12:7 13:2 22:7 23:2 34:7 37:2 45:7 46:2 118:0
40: 0:0 2:1 9:2 20:1 21:2 34:1 37:2 44:1 49:2 118:0
41: 0:0 2:1 3:2 12:1 13:2 22:1 23:2 34:1 37:2 45:1 46:2 118:0

View File

@@ -1,5 +1,6 @@
---
source: tui/src/analytics/styles_tests.rs
assertion_line: 145
expression: snapshot
---
120x42
@@ -98,5 +99,5 @@ Rows (column:palette):
37: 0:0 2:6 4:0
38: 0:0
39: 0:0
40: 0:0 2:5 9:2 20:5 21:2 34:5 37:2 44:5 49:2 118:0
41: 0:0 2:5 3:2 12:5 13:2 24:5 27:2 35:5 36:2 118:0
40: 0:0 2:1 9:2 20:1 21:2 34:1 37:2 44:1 49:2 118:0
41: 0:0 2:1 3:2 12:1 13:2 24:1 27:2 35:1 36:2 118:0

View File

@@ -1,5 +1,6 @@
---
source: tui/src/analytics/styles_tests.rs
assertion_line: 145
expression: snapshot
---
120x42
@@ -98,5 +99,5 @@ Rows (column:palette):
37: 0:0 2:5 4:0
38: 0:0
39: 0:0
40: 0:0 2:7 9:2 20:7 21:2 34:7 37:2 44:7 49:2 118:0
41: 0:0 2:7 3:2 12:7 13:2 24:7 27:2 35:7 36:2 118:0
40: 0:0 2:1 9:2 20:1 21:2 34:1 37:2 44:1 49:2 118:0
41: 0:0 2:1 3:2 12:1 13:2 24:1 27:2 35:1 36:2 118:0

View File

@@ -1,5 +1,6 @@
---
source: tui/src/analytics/styles_tests.rs
assertion_line: 145
expression: snapshot
---
120x42
@@ -52,7 +53,6 @@ Palette (foreground|background|underline|modifiers):
2: Rgb(165, 165, 165)|Reset|Reset|NONE
3: Cyan|Reset|Reset|BOLD
4: Reset|Reset|Reset|DIM
5: White|Reset|Reset|BOLD
Rows (column:palette):
0: 0:0 2:1 11:2 28:0
1: 0:0
@@ -94,5 +94,5 @@ Rows (column:palette):
37: 0:0
38: 0:0
39: 0:0
40: 0:0 2:5 9:2 20:5 21:2 34:5 37:2 44:5 49:2 118:0
41: 0:0 2:5 3:2 14:5 17:2 25:5 26:2 118:0
40: 0:0 2:1 9:2 20:1 21:2 34:1 37:2 44:1 49:2 118:0
41: 0:0 2:1 3:2 14:1 17:2 25:1 26:2 118:0

View File

@@ -1,5 +1,6 @@
---
source: tui/src/analytics/styles_tests.rs
assertion_line: 145
expression: snapshot
---
120x42
@@ -98,5 +99,5 @@ Rows (column:palette):
37: 0:0 2:6 4:0
38: 0:0
39: 0:0
40: 0:0 2:5 9:2 20:5 21:2 34:5 37:2 44:5 49:2 118:0
41: 0:0 2:5 3:2 12:5 13:2 24:5 27:2 35:5 36:2 118:0
40: 0:0 2:1 9:2 20:1 21:2 34:1 37:2 44:1 49:2 118:0
41: 0:0 2:1 3:2 12:1 13:2 24:1 27:2 35:1 36:2 118:0

View File

@@ -1,5 +1,6 @@
---
source: tui/src/analytics/styles_tests.rs
assertion_line: 145
expression: snapshot
---
120x42
@@ -52,7 +53,6 @@ Palette (foreground|background|underline|modifiers):
2: Rgb(165, 165, 165)|Reset|Reset|NONE
3: Cyan|Reset|Reset|BOLD
4: Reset|Reset|Reset|DIM
5: White|Reset|Reset|BOLD
Rows (column:palette):
0: 0:0 2:1 11:2 28:0
1: 0:0
@@ -94,5 +94,5 @@ Rows (column:palette):
37: 0:0
38: 0:0
39: 0:0
40: 0:0 2:5 9:2 20:5 21:2 118:0
41: 0:0 2:5 3:2 11:5 12:2 23:5 26:2 34:5 35:2 118:0
40: 0:0 2:1 9:2 20:1 21:2 118:0
41: 0:0 2:1 3:2 11:1 12:2 23:1 26:2 34:1 35:2 118:0

View File

@@ -1,5 +1,6 @@
---
source: tui/src/analytics/styles_tests.rs
assertion_line: 145
expression: snapshot
---
120x42
@@ -98,5 +99,5 @@ Rows (column:palette):
37: 0:0 2:5 4:0
38: 0:0
39: 0:0
40: 0:0 2:6 9:2 20:6 21:2 34:6 37:2 44:6 49:2 118:0
41: 0:0 2:6 3:2 12:6 13:2 22:6 23:2 34:6 37:2 45:6 46:2 118:0
40: 0:0 2:1 9:2 20:1 21:2 34:1 37:2 44:1 49:2 118:0
41: 0:0 2:1 3:2 12:1 13:2 22:1 23:2 34:1 37:2 45:1 46:2 118:0

View File

@@ -1,5 +1,6 @@
---
source: tui/src/analytics/styles_tests.rs
assertion_line: 145
expression: snapshot
---
120x42
@@ -98,5 +99,5 @@ Rows (column:palette):
37: 0:0 2:6 4:0
38: 0:0
39: 0:0
40: 0:0 2:5 9:2 20:5 21:2 34:5 37:2 44:5 49:2 118:0
41: 0:0 2:5 3:2 12:5 13:2 24:5 27:2 35:5 36:2 118:0
40: 0:0 2:1 9:2 20:1 21:2 34:1 37:2 44:1 49:2 118:0
41: 0:0 2:1 3:2 12:1 13:2 24:1 27:2 35:1 36:2 118:0

View File

@@ -1,5 +1,6 @@
---
source: tui/src/analytics/styles_tests.rs
assertion_line: 145
expression: snapshot
---
120x42
@@ -52,7 +53,6 @@ Palette (foreground|background|underline|modifiers):
2: Rgb(165, 165, 165)|Reset|Reset|NONE
3: Cyan|Reset|Reset|BOLD
4: Reset|Reset|Reset|DIM
5: White|Reset|Reset|BOLD
Rows (column:palette):
0: 0:0 2:1 11:2 28:0
1: 0:0
@@ -94,5 +94,5 @@ Rows (column:palette):
37: 0:0
38: 0:0
39: 0:0
40: 0:0 2:5 9:2 20:5 21:2 29:5 30:2 43:5 46:2 53:5 58:2 118:0
41: 0:0 2:5 3:2 14:5 17:2 25:5 26:2 118:0
40: 0:0 2:1 9:2 20:1 21:2 29:1 30:2 43:1 46:2 53:1 58:2 118:0
41: 0:0 2:1 3:2 14:1 17:2 25:1 26:2 118:0

View File

@@ -1,5 +1,6 @@
---
source: tui/src/analytics/styles_tests.rs
assertion_line: 145
expression: snapshot
---
120x42
@@ -98,5 +99,5 @@ Rows (column:palette):
37: 0:0 2:6 4:0
38: 0:0
39: 0:0
40: 0:0 2:5 9:2 20:5 21:2 34:5 37:2 44:5 49:2 118:0
41: 0:0 2:5 3:2 12:5 13:2 22:5 23:2 34:5 37:2 45:5 46:2 118:0
40: 0:0 2:1 9:2 20:1 21:2 34:1 37:2 44:1 49:2 118:0
41: 0:0 2:1 3:2 12:1 13:2 22:1 23:2 34:1 37:2 45:1 46:2 118:0

View File

@@ -1,5 +1,6 @@
---
source: tui/src/analytics/styles_tests.rs
assertion_line: 145
expression: snapshot
---
120x42
@@ -98,5 +99,5 @@ Rows (column:palette):
37: 0:0 2:6 4:0
38: 0:0
39: 0:0
40: 0:0 2:5 9:2 20:5 21:2 34:5 37:2 44:5 49:2 118:0
41: 0:0 2:5 3:2 12:5 13:2 24:5 27:2 35:5 36:2 118:0
40: 0:0 2:1 9:2 20:1 21:2 34:1 37:2 44:1 49:2 118:0
41: 0:0 2:1 3:2 12:1 13:2 24:1 27:2 35:1 36:2 118:0

View File

@@ -1,5 +1,6 @@
---
source: tui/src/analytics/styles_tests.rs
assertion_line: 145
expression: snapshot
---
120x42
@@ -52,7 +53,6 @@ Palette (foreground|background|underline|modifiers):
2: Rgb(165, 165, 165)|Reset|Reset|NONE
3: Cyan|Reset|Reset|BOLD
4: Reset|Reset|Reset|DIM
5: White|Reset|Reset|BOLD
Rows (column:palette):
0: 0:0 2:1 11:2 28:0
1: 0:0
@@ -94,5 +94,5 @@ Rows (column:palette):
37: 0:0
38: 0:0
39: 0:0
40: 0:0 2:5 9:2 20:5 21:2 118:0
41: 0:0 2:5 3:2 11:5 12:2 23:5 26:2 34:5 35:2 118:0
40: 0:0 2:1 9:2 20:1 21:2 118:0
41: 0:0 2:1 3:2 11:1 12:2 23:1 26:2 34:1 35:2 118:0

View File

@@ -1,5 +1,6 @@
---
source: tui/src/analytics/styles_tests.rs
assertion_line: 145
expression: snapshot
---
120x42
@@ -56,7 +57,7 @@ Palette (foreground|background|underline|modifiers):
6: Magenta|Reset|Reset|NONE
7: Cyan|Reset|Reset|NONE
8: Rgb(0, 95, 135)|Reset|Reset|BOLD | UNDERLINED
9: White|Reset|Reset|BOLD
9: Black|Reset|Reset|BOLD
Rows (column:palette):
0: 0:0 2:1 11:2 28:0
1: 0:0 2:3 6:0 11:2 34:0

View File

@@ -1,5 +1,6 @@
---
source: tui/src/analytics/styles_tests.rs
assertion_line: 145
expression: snapshot
---
120x42
@@ -56,7 +57,7 @@ Palette (foreground|background|underline|modifiers):
6: Magenta|Reset|Reset|NONE
7: Cyan|Reset|Reset|NONE
8: Rgb(0, 95, 135)|Reset|Reset|BOLD | UNDERLINED
9: White|Reset|Reset|BOLD
9: Black|Reset|Reset|BOLD
Rows (column:palette):
0: 0:0 2:1 11:2 28:0
1: 0:0 2:3 6:0 11:2 34:0

View File

@@ -1,5 +1,6 @@
---
source: tui/src/analytics/styles_tests.rs
assertion_line: 145
expression: snapshot
---
120x42
@@ -56,7 +57,7 @@ Palette (foreground|background|underline|modifiers):
6: Magenta|Reset|Reset|NONE
7: Cyan|Reset|Reset|NONE
8: Rgb(0, 95, 135)|Reset|Reset|BOLD | UNDERLINED
9: White|Reset|Reset|BOLD
9: Black|Reset|Reset|BOLD
Rows (column:palette):
0: 0:0 2:1 11:2 28:0
1: 0:0 2:3 6:0 11:2 34:0

View File

@@ -1,5 +1,6 @@
---
source: tui/src/analytics/styles_tests.rs
assertion_line: 145
expression: snapshot
---
120x42
@@ -52,7 +53,7 @@ Palette (foreground|background|underline|modifiers):
2: Rgb(76, 76, 76)|Reset|Reset|NONE
3: Rgb(0, 95, 135)|Reset|Reset|BOLD
4: Reset|Reset|Reset|DIM
5: White|Reset|Reset|BOLD
5: Black|Reset|Reset|BOLD
Rows (column:palette):
0: 0:0 2:1 11:2 28:0
1: 0:0

View File

@@ -1,5 +1,6 @@
---
source: tui/src/analytics/styles_tests.rs
assertion_line: 145
expression: snapshot
---
120x42
@@ -56,7 +57,7 @@ Palette (foreground|background|underline|modifiers):
6: Magenta|Reset|Reset|NONE
7: Cyan|Reset|Reset|NONE
8: Rgb(0, 95, 135)|Reset|Reset|BOLD | UNDERLINED
9: White|Reset|Reset|BOLD
9: Black|Reset|Reset|BOLD
Rows (column:palette):
0: 0:0 2:1 11:2 28:0
1: 0:0 2:3 6:0 11:2 34:0

View File

@@ -1,5 +1,6 @@
---
source: tui/src/analytics/styles_tests.rs
assertion_line: 145
expression: snapshot
---
120x42
@@ -52,7 +53,7 @@ Palette (foreground|background|underline|modifiers):
2: Rgb(76, 76, 76)|Reset|Reset|NONE
3: Rgb(0, 95, 135)|Reset|Reset|BOLD
4: Reset|Reset|Reset|DIM
5: White|Reset|Reset|BOLD
5: Black|Reset|Reset|BOLD
Rows (column:palette):
0: 0:0 2:1 11:2 28:0
1: 0:0

View File

@@ -1,5 +1,6 @@
---
source: tui/src/analytics/styles_tests.rs
assertion_line: 145
expression: snapshot
---
120x42
@@ -56,7 +57,7 @@ Palette (foreground|background|underline|modifiers):
6: Magenta|Reset|Reset|NONE
7: Cyan|Reset|Reset|NONE
8: Rgb(0, 95, 135)|Reset|Reset|BOLD | UNDERLINED
9: White|Reset|Reset|BOLD
9: Black|Reset|Reset|BOLD
Rows (column:palette):
0: 0:0 2:1 11:2 28:0
1: 0:0 2:3 6:0 11:2 34:0

View File

@@ -1,5 +1,6 @@
---
source: tui/src/analytics/styles_tests.rs
assertion_line: 145
expression: snapshot
---
120x42
@@ -56,7 +57,7 @@ Palette (foreground|background|underline|modifiers):
6: Magenta|Reset|Reset|NONE
7: Cyan|Reset|Reset|NONE
8: Rgb(0, 95, 135)|Reset|Reset|BOLD | UNDERLINED
9: White|Reset|Reset|BOLD
9: Black|Reset|Reset|BOLD
Rows (column:palette):
0: 0:0 2:1 11:2 28:0
1: 0:0 2:3 6:0 11:2 34:0

View File

@@ -1,5 +1,6 @@
---
source: tui/src/analytics/styles_tests.rs
assertion_line: 145
expression: snapshot
---
120x42
@@ -52,7 +53,7 @@ Palette (foreground|background|underline|modifiers):
2: Rgb(76, 76, 76)|Reset|Reset|NONE
3: Rgb(0, 95, 135)|Reset|Reset|BOLD
4: Reset|Reset|Reset|DIM
5: White|Reset|Reset|BOLD
5: Black|Reset|Reset|BOLD
Rows (column:palette):
0: 0:0 2:1 11:2 28:0
1: 0:0

View File

@@ -1,5 +1,6 @@
---
source: tui/src/analytics/styles_tests.rs
assertion_line: 145
expression: snapshot
---
120x42
@@ -56,7 +57,7 @@ Palette (foreground|background|underline|modifiers):
6: Magenta|Reset|Reset|NONE
7: Cyan|Reset|Reset|NONE
8: Rgb(0, 95, 135)|Reset|Reset|BOLD | UNDERLINED
9: White|Reset|Reset|BOLD
9: Black|Reset|Reset|BOLD
Rows (column:palette):
0: 0:0 2:1 11:2 28:0
1: 0:0 2:3 6:0 11:2 34:0

View File

@@ -1,5 +1,6 @@
---
source: tui/src/analytics/styles_tests.rs
assertion_line: 145
expression: snapshot
---
120x42
@@ -56,7 +57,7 @@ Palette (foreground|background|underline|modifiers):
6: Magenta|Reset|Reset|NONE
7: Cyan|Reset|Reset|NONE
8: Rgb(0, 95, 135)|Reset|Reset|BOLD | UNDERLINED
9: White|Reset|Reset|BOLD
9: Black|Reset|Reset|BOLD
Rows (column:palette):
0: 0:0 2:1 11:2 28:0
1: 0:0 2:3 6:0 11:2 34:0

View File

@@ -1,5 +1,6 @@
---
source: tui/src/analytics/styles_tests.rs
assertion_line: 145
expression: snapshot
---
120x42
@@ -52,7 +53,7 @@ Palette (foreground|background|underline|modifiers):
2: Rgb(76, 76, 76)|Reset|Reset|NONE
3: Rgb(0, 95, 135)|Reset|Reset|BOLD
4: Reset|Reset|Reset|DIM
5: White|Reset|Reset|BOLD
5: Black|Reset|Reset|BOLD
Rows (column:palette):
0: 0:0 2:1 11:2 28:0
1: 0:0

View File

@@ -79,19 +79,26 @@ fn analytics_sections_adapt_to_terminal_colors() {
assert!(marker.modifier.contains(Modifier::BOLD));
}
if theme == "light" {
for cell in &buffer.content {
if !cell.symbol().trim().is_empty()
&& let Color::Rgb(r, g, b) = cell.fg
{
let contrast =
(luminance(colors.bg) + 0.05) / (luminance((r, g, b)) + 0.05);
assert!(
contrast >= 4.5,
"{}: {:?} has contrast {contrast}",
cell.symbol(),
cell.fg
);
}
for cell in buffer
.content
.iter()
.filter(|cell| !cell.symbol().trim().is_empty())
{
let foreground = match cell.fg {
Color::Rgb(r, g, b) => (r, g, b),
Color::White => (255, 255, 255),
Color::Black => (0, 0, 0),
Color::Reset => colors.fg,
_ => continue,
};
let contrast =
(luminance(colors.bg) + 0.05) / (luminance(foreground) + 0.05);
assert!(
contrast >= 4.5,
"{}: {:?} has contrast {contrast}",
cell.symbol(),
cell.fg
);
}
}
// Preserve every symbol and style while sharing repeated styles in a palette.

View File

@@ -11,6 +11,9 @@ mod summary;
#[path = "analytics/account_layout_tests.rs"]
mod account_layout;
#[path = "analytics/navigation_tests.rs"]
mod navigation;
#[path = "analytics/consumer_refresh_tests.rs"]
mod consumer_refresh;

View File

@@ -1,6 +1,27 @@
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn experimental_features_analytics_plan_history() {
let feature = Feature::AnalyticsPlanHistory;
let stage = feature.stage();
let (app_tx, _app_rx) = tokio::sync::mpsc::unbounded_channel();
let view = ExperimentalFeaturesView::new(
vec![ExperimentalFeatureItem {
key: feature.key().to_string(),
name: stage.experimental_menu_name().unwrap().to_string(),
description: stage.experimental_menu_description().unwrap().to_string(),
enabled: feature.default_enabled(),
writable: true,
}],
ThreadId::new(),
/*catalog_rx*/ None,
AppEventSender::new(app_tx),
crate::keymap::RuntimeKeymap::defaults().list,
);
snapshot_view("experimental_features_analytics_plan_history", &view);
}
fn server_feature(name: &str) -> ExperimentalFeature {
ExperimentalFeature {
name: name.to_string(),

View File

@@ -0,0 +1,13 @@
---
source: tui/src/bottom_pane/experimental_features_view_tests.rs
expression: buffer_text(&buffer)
---
Experimental features
Checked features are configured on. Some experimental features take effect
only in new tasks or after restarting the Codex server.
[ ] Analytics plan history Preview five-hour and weekly allowance history
for consumer accounts in /analytics.
Press space to select or enter to save