mirror of
https://github.com/openai/codex.git
synced 2026-09-17 12:23:33 +00:00
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:
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
272
codex-rs/tui/src/analytics/navigation_tests.rs
Normal file
272
codex-rs/tui/src/analytics/navigation_tests.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
---
|
||||
source: tui/src/analytics/navigation_tests.rs
|
||||
assertion_line: 60
|
||||
expression: output
|
||||
---
|
||||
" Analytics · Live account "
|
||||
" [7d] 30d · Aug 27–Sep 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/1–6 section · z dashboard · ←/→ day · enter details "
|
||||
" r 7/30d · g group · R refresh · esc back · q close "
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user