From 39b8c8688317f3e03a7a27c9f65b20c9934ab56a Mon Sep 17 00:00:00 2001 From: woffko <2505149+woffko@users.noreply.github.com> Date: Wed, 22 Jul 2026 21:01:15 +0300 Subject: [PATCH 1/4] Polish locale controls and quit flow --- CHANGELOG.md | 5 +- README.md | 6 +- docs/screenshots/usage-system-full.png | Bin 0 -> 113975 bytes src/app/mod.rs | 60 ++++++- src/locale.rs | 22 ++- src/ui/mod.rs | 240 +++++++++++++++++++++---- 6 files changed, 282 insertions(+), 51 deletions(-) create mode 100644 docs/screenshots/usage-system-full.png diff --git a/CHANGELOG.md b/CHANGELOG.md index 06e582c..edaabd9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,10 +7,13 @@ All notable changes to this project are documented in this file. - Added a persisted `n` shortcut to cycle display formatting through Classic, System Compact, and System Full for numbers, dates, times, and calendar labels. - Clarified token pair charts with the `TOKENS (TOTAL / NON-CACHED)` heading. - Added compact `K/M/B/T` notation for large dashboard token values in System Compact mode, including values inside vertical chart bars. System Full keeps those values expanded, while Classic output remains unchanged. -- Added mouse controls for selecting the visible statistic, chart timeframe, and chart orientation; clicking the footer cycles the display style on every screen. +- Added mouse controls for selecting the visible statistic, chart timeframe, chart orientation, screen, and display style. - Restored compact one-line control strips on the Usage and Activity screens and highlighted the padded group labels with the chart's darker cyan. - Removed duplicate shortcut hints from screen headers, leaving the complete shortcut reference in the footer and help overlay. - Added clickable screen tabs to the outer title and matching framed View/Projects controls to the Activity screen. +- System Full now groups expanded integers with regular spaces while retaining system-localized dates, times, and decimals. +- Added a compact `STYLE CLASS/SCOMP/SFULL` selector after the Usage bar controls and a bottom-border Quit action with a safe Yes/No confirmation dialog; the `q` shortcut opens the same dialog. +- Added the current locale-aware date and time to the `TODAY` card border. ## 0.3.6 - 2026-06-05 diff --git a/README.md b/README.md index 4fceb41..36dc425 100644 --- a/README.md +++ b/README.md @@ -133,11 +133,11 @@ comon --scan-time-budget-ms 1500 --max-jsonl-line-kib 512 - `w` Toggle timeframe (Week/Month) - `f` Toggle layout (Horz/Vert) - `n` Cycle display formatting (Classic/System Compact/System Full) -- Mouse: click the top tabs to switch screens; click Tokens/Time/Runs, Week/Month, Horz/Vert, or Activity project-count controls; click the footer to cycle display formatting +- Mouse: click the top tabs, Usage/Activity controls (including the Usage style selector), or the bottom-right Quit action - `s` / `F2` Switch between Usage and Session history - `r` / `F5` Refresh current screen - `?` Help overlay -- `Esc` / `q` Quit +- `q` Quit (with confirmation) - `Enter` / `y` Continue past "no sessions found" warning (when shown) - Session history: `Up` / `Down` / mouse wheel navigate, `Enter` / `Right` open project sessions, `Backspace` / `Left` / `Esc` go back @@ -342,7 +342,7 @@ CI also runs this check on each push and pull request via `.github/workflows/asc - Usage stats are derived from Codex session JSONL logs. If you have no session data yet, values will be empty. - Limits/credits require Codex App Server to start successfully (auth, environment, and a usable working directory). CoMon auto-detects `codex` on `PATH` and common Windows Codex App bundle locations; use `--codex-bin` or `--app-server-bin` when needed. - comon stores local app state in `~/.comon/state.json` by default (or `$COMON_HOME`, or `--comon-home`). -- Display formatting starts in Classic mode; press `n` to cycle through Classic, System Compact, and System Full. Both System modes use the operating system locale for numbers, dates, times, and calendar labels; Compact abbreviates dashboard token values, while Full keeps them expanded except for the pre-existing compact summary values. The choice is saved in `state.json` without changing stored data. +- Display formatting starts in Classic mode; press `n` or use `STYLE CLASS/SCOMP/SFULL` in the Usage controls to choose Classic, System Compact, or System Full. Both System modes use the operating system locale for dates, times, decimals, and calendar labels; Compact uses the detected thousands separator and abbreviates dashboard token values, while Full groups expanded integers with regular spaces. The choice is saved in `state.json` without changing stored data. - comon stores scan cache in `~/.comon/comon.db` to avoid rereading unchanged session files. - Large session logs are parsed incrementally with persisted parser offsets in `comon.db`; unchanged files are reused from cache. - If historical days look incomplete after adding old session files, run once with `--full-scan --scan-time-budget-ms 0` to force a full reparse and refresh cached summaries. diff --git a/docs/screenshots/usage-system-full.png b/docs/screenshots/usage-system-full.png new file mode 100644 index 0000000000000000000000000000000000000000..e5917466353105e64812c97f826ef4188f863389 GIT binary patch literal 113975 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 literal 0 HcmV?d00001 diff --git a/src/app/mod.rs b/src/app/mod.rs index a4facb7..173cfc8 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -74,12 +74,15 @@ pub(crate) enum ActiveScreen { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum UiClickAction { SetScreen(ActiveScreen), + SetDisplayStyle(DisplayStyle), SetMetric(UsageMetric), SetRange(ChartRange), SetOrientation(ChartOrientation), DecreaseProjects, IncreaseProjects, - CycleDisplayStyle, + PromptQuit, + CancelQuit, + ConfirmQuit, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -138,6 +141,7 @@ pub(crate) struct AppState { pub(crate) workspace_path: Option, pub(crate) no_sessions_confirm_open: bool, pub(crate) no_sessions_confirm_dismissed: bool, + pub(crate) quit_confirm_open: bool, pub(crate) display_style: DisplayStyle, pub(crate) system_locale: SystemLocale, pub(crate) ui_hit_targets: Vec, @@ -479,6 +483,7 @@ async fn run_inner( workspace_path: restored_ui_state.workspace_path.clone(), no_sessions_confirm_open: false, no_sessions_confirm_dismissed: restored_ui_state.no_sessions_confirm_dismissed, + quit_confirm_open: false, display_style: restored_ui_state.display_style, system_locale: config.system_locale.clone(), ui_hit_targets: Vec::new(), @@ -603,6 +608,37 @@ fn handle_input_event( usage_refresh_tx: &mpsc::Sender<()>, limits_refresh_tx: &mpsc::Sender<()>, ) -> Result { + if state.quit_confirm_open { + return match event { + Event::Mouse(mouse) if mouse.kind == MouseEventKind::Down(MouseButton::Left) => { + match ui_click_action_at(&state.ui_hit_targets, mouse.column, mouse.row) { + Some(UiClickAction::ConfirmQuit) => Ok(InputOutcome::Quit), + Some(UiClickAction::CancelQuit) => { + state.quit_confirm_open = false; + Ok(InputOutcome::Continue(true)) + } + _ => Ok(InputOutcome::Continue(false)), + } + } + Event::Key(key) if key.kind == KeyEventKind::Press => match (key.code, key.modifiers) { + (KeyCode::Char('y'), _) | (KeyCode::Char('Y'), _) => Ok(InputOutcome::Quit), + (KeyCode::Char('c'), KeyModifiers::CONTROL) => Ok(InputOutcome::Quit), + (KeyCode::Enter, _) + | (KeyCode::Esc, _) + | (KeyCode::Char('n'), _) + | (KeyCode::Char('N'), _) + | (KeyCode::Char('q'), _) + | (KeyCode::Char('Q'), _) => { + state.quit_confirm_open = false; + Ok(InputOutcome::Continue(true)) + } + _ => Ok(InputOutcome::Continue(false)), + }, + Event::Resize(_, _) => Ok(InputOutcome::Continue(true)), + _ => Ok(InputOutcome::Continue(false)), + }; + } + if let Event::Mouse(mouse) = &event { if state.show_help || state.no_sessions_confirm_open { return Ok(InputOutcome::Continue(false)); @@ -610,6 +646,9 @@ fn handle_input_event( if mouse.kind == MouseEventKind::Down(MouseButton::Left) { if let Some(action) = ui_click_action_at(&state.ui_hit_targets, mouse.column, mouse.row) { + if action == UiClickAction::ConfirmQuit { + return Ok(InputOutcome::Quit); + } let changed = apply_ui_click_action(state, action); return Ok(InputOutcome::Continue(changed)); } @@ -622,7 +661,10 @@ fn handle_input_event( } match (key.code, key.modifiers) { - (KeyCode::Char('q'), _) => return Ok(InputOutcome::Quit), + (KeyCode::Char('q'), _) | (KeyCode::Char('Q'), _) => { + state.quit_confirm_open = true; + return Ok(InputOutcome::Continue(true)); + } (KeyCode::Char('c'), KeyModifiers::CONTROL) => return Ok(InputOutcome::Quit), (KeyCode::Char('s'), _) | (KeyCode::Char('S'), _) | (KeyCode::F(2), _) => { state.active_screen = next_active_screen(state.active_screen); @@ -693,6 +735,11 @@ fn apply_ui_click_action(state: &mut AppState, action: UiClickAction) -> bool { state.active_screen = screen; changed } + UiClickAction::SetDisplayStyle(style) => { + let changed = state.display_style != style; + state.display_style = style; + changed + } UiClickAction::SetMetric(metric) => { let changed = state.metric != metric; state.metric = metric; @@ -726,10 +773,15 @@ fn apply_ui_click_action(state: &mut AppState, action: UiClickAction) -> bool { state.activity_project_limit = next; changed } - UiClickAction::CycleDisplayStyle => { - state.display_style = state.display_style.toggled(); + UiClickAction::PromptQuit => { + state.quit_confirm_open = true; + true + } + UiClickAction::CancelQuit => { + state.quit_confirm_open = false; true } + UiClickAction::ConfirmQuit => false, } } diff --git a/src/locale.rs b/src/locale.rs index 6e3d1ec..60c8de3 100644 --- a/src/locale.rs +++ b/src/locale.rs @@ -109,7 +109,9 @@ impl<'a> DisplayFormatter<'a> { DisplayStyle::SystemCompact => { format!("System Compact ({})", self.system.id()) } - DisplayStyle::SystemFull => format!("System Full ({})", self.system.id()), + DisplayStyle::SystemFull => { + format!("System Full ({}, space grouped)", self.system.id()) + } } } @@ -124,9 +126,8 @@ impl<'a> DisplayFormatter<'a> { pub(crate) fn format_u64(self, value: u64) -> String { let separator = match self.style { DisplayStyle::Classic => ",", - DisplayStyle::SystemCompact | DisplayStyle::SystemFull => { - self.system.thousands_separator.as_str() - } + DisplayStyle::SystemCompact => self.system.thousands_separator.as_str(), + DisplayStyle::SystemFull => " ", }; group_decimal_digits(value, separator) } @@ -671,6 +672,19 @@ mod tests { assert_eq!(formatter.localize_decimal("1.23M"), "1,23M"); } + #[test] + fn system_full_uses_regular_spaces_for_digit_grouping() { + let system = european_profile(); + let formatter = DisplayFormatter::new(DisplayStyle::SystemFull, &system); + + assert_eq!(formatter.format_count(1_234_567), "1 234 567"); + assert_eq!(formatter.localize_decimal("1.23"), "1,23"); + assert_eq!( + formatter.style_label(), + "System Full (et-EE, space grouped)" + ); + } + #[test] fn system_formats_dates_and_time_from_profile() { let system = european_profile(); diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 4cc48b7..acc5c66 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -5,7 +5,7 @@ use crate::usage::{ format_tokens_overview, ChartRange, ProjectActivity, UsageMetric, ACTIVITY_TIMELINE_WEEKS, }; use anyhow::Result; -use chrono::{Datelike, Local, NaiveDate, TimeZone, Weekday}; +use chrono::{Datelike, Local, NaiveDate, NaiveDateTime, TimeZone, Weekday}; use crossterm::{ event::{DisableMouseCapture, EnableMouseCapture}, execute, @@ -78,6 +78,8 @@ pub fn render(frame: &mut Frame<'_>, state: &mut AppState) { let area = frame.area(); let (navigation, navigation_targets) = navigation_title(area, state.active_screen); state.ui_hit_targets.extend(navigation_targets); + let (quit, quit_targets) = quit_title(area, state.quit_confirm_open); + state.ui_hit_targets.extend(quit_targets); let outer = Block::default() .borders(Borders::ALL) @@ -86,7 +88,8 @@ pub fn render(frame: &mut Frame<'_>, state: &mut AppState) { format!(" comon :: {} ", env!("CARGO_PKG_VERSION")), Style::default().add_modifier(Modifier::BOLD), )) - .title_top(navigation.right_aligned()); + .title_top(navigation.right_aligned()) + .title_bottom(quit.right_aligned()); frame.render_widget(outer, area); let inner = apply_margin( @@ -152,20 +155,15 @@ pub fn render(frame: &mut Frame<'_>, state: &mut AppState) { } } ActiveScreen::Read => { - let chunks = Layout::default() - .direction(Direction::Vertical) - .constraints([ - Constraint::Length(3), - Constraint::Min(0), - Constraint::Length(2), - ]) - .split(inner); - register_hit_target(state, chunks[2], UiClickAction::CycleDisplayStyle); let system_locale = state.system_locale.clone(); let formatter = DisplayFormatter::new(state.display_style, &system_locale); crate::read::tui::render(frame, inner, &mut state.read_browser, formatter); } } + + if state.quit_confirm_open { + render_quit_confirmation(frame, area, state); + } } fn usage_screen_layout(area: Rect, footer_height: u16) -> [Rect; 3] { @@ -230,6 +228,18 @@ fn navigation_title(area: Rect, active_screen: ActiveScreen) -> (Line<'static>, (line, right_aligned_targets(title_area, &segments)) } +fn quit_title(area: Rect, active: bool) -> (Line<'static>, Vec) { + let title_area = Rect::new( + area.x.saturating_add(1), + area.y.saturating_add(area.height.saturating_sub(1)), + area.width.saturating_sub(2), + 1, + ); + let segments = [(" QUIT ", Some(UiClickAction::PromptQuit)), (" ", None)]; + let line = Line::from(vec![pill("QUIT", active), Span::raw(" ")]); + (line, right_aligned_targets(title_area, &segments)) +} + fn render_header(frame: &mut Frame<'_>, area: Rect, state: &AppState) { let line_area = usage_header_line_area(area); let row = Layout::default() @@ -347,7 +357,7 @@ fn footer_height(width: u16, state: &AppState) -> u16 { wrapped_line_count(&footer_text(state), width).max(1) } -fn render_footer(frame: &mut Frame<'_>, area: Rect, state: &mut AppState) { +fn render_footer(frame: &mut Frame<'_>, area: Rect, state: &AppState) { let err = footer_error(state); let style = format!("Style [n]: {}", state.formatter().style_label()); let line = Line::from(vec![ @@ -362,7 +372,6 @@ fn render_footer(frame: &mut Frame<'_>, area: Rect, state: &mut AppState) { ]); frame.render_widget(Paragraph::new(line).wrap(Wrap { trim: true }), area); - register_hit_target(state, area, UiClickAction::CycleDisplayStyle); } fn render_usage(frame: &mut Frame<'_>, area: Rect, state: &mut AppState) { @@ -922,13 +931,27 @@ fn render_usage_controls( let month = pill("MONTH", state.range == ChartRange::Month); let vert = pill("VERT", state.orientation == ChartOrientation::Vertical); let horz = pill("HORZ", state.orientation == ChartOrientation::Horizontal); + let classic = pill("CLASS", state.display_style == DisplayStyle::Classic); + let system_compact = pill("SCOMP", state.display_style == DisplayStyle::SystemCompact); + let system_full = pill("SFULL", state.display_style == DisplayStyle::SystemFull); render_flat_usage_controls( frame, chunks[0], state, &workspace_label, - vec![tokens, time, runs, week, month, vert, horz], + vec![ + tokens, + time, + runs, + week, + month, + vert, + horz, + classic, + system_compact, + system_full, + ], ); if let Some(text) = reset_summary { @@ -957,7 +980,7 @@ fn render_flat_usage_controls( ) { let row = Layout::default() .direction(Direction::Horizontal) - .constraints([Constraint::Min(20), Constraint::Min(0)]) + .constraints([Constraint::Min(20), Constraint::Length(92)]) .split(area); render_workspace_label(frame, row[0], workspace_label); @@ -984,6 +1007,19 @@ fn render_flat_usage_controls( " HORZ ", Some(UiClickAction::SetOrientation(ChartOrientation::Horizontal)), ), + (" STYLE ", None), + ( + " CLASS ", + Some(UiClickAction::SetDisplayStyle(DisplayStyle::Classic)), + ), + ( + " SCOMP ", + Some(UiClickAction::SetDisplayStyle(DisplayStyle::SystemCompact)), + ), + ( + " SFULL ", + Some(UiClickAction::SetDisplayStyle(DisplayStyle::SystemFull)), + ), ], ); @@ -993,6 +1029,8 @@ fn render_flat_usage_controls( spans.extend(pills[3..5].iter().cloned()); spans.push(control_group_label("BARS")); spans.extend(pills[5..7].iter().cloned()); + spans.push(control_group_label("STYLE")); + spans.extend(pills[7..10].iter().cloned()); frame.render_widget( Paragraph::new(Line::from(spans)).alignment(Alignment::Right), row[1], @@ -1325,6 +1363,7 @@ fn usage_cards_height(state: &AppState, width: u16) -> u16 { fn render_usage_cards(frame: &mut Frame<'_>, area: Rect, state: &AppState) { let formatter = state.formatter(); + let today_title = today_card_title(formatter, Local::now().naive_local()); let card_layout = usage_card_layout(area.width); let row_heights = usage_card_row_heights(state, area.width); let two_rows = row_heights.len() > 1; @@ -1457,7 +1496,7 @@ fn render_usage_cards(frame: &mut Frame<'_>, area: Rect, state: &AppState) { ); frame.render_widget( card( - "TODAY", + &today_title, &today_value, Some(&today_caption1), Some(&today_caption2), @@ -1511,7 +1550,7 @@ fn render_usage_cards(frame: &mut Frame<'_>, area: Rect, state: &AppState) { let runs7 = last7_runs_caption.as_deref(); frame.render_widget( card( - "TODAY", + &today_title, &today_value, Some(&today_caption1), Some(&today_caption2), @@ -1611,7 +1650,7 @@ fn render_usage_cards(frame: &mut Frame<'_>, area: Rect, state: &AppState) { ); frame.render_widget( card( - "TODAY", + &today_title, &today_value, Some(&today_caption1), Some(&today_caption2), @@ -1662,7 +1701,7 @@ fn render_usage_cards(frame: &mut Frame<'_>, area: Rect, state: &AppState) { let runs7 = last7_runs_caption.as_deref(); frame.render_widget( card( - "TODAY", + &today_title, &today_value, Some(&today_caption1), Some(&today_caption2), @@ -1789,7 +1828,7 @@ fn render_usage_cards(frame: &mut Frame<'_>, area: Rect, state: &AppState) { ); frame.render_widget( card( - "TODAY", + &today_title, &today_value, Some(&today_caption1), Some(&today_caption2), @@ -1837,7 +1876,7 @@ fn render_usage_cards(frame: &mut Frame<'_>, area: Rect, state: &AppState) { ); frame.render_widget( card( - "TODAY", + &today_title, &today_value, Some(&today_caption1), Some(&today_caption2), @@ -2455,11 +2494,11 @@ fn render_help_overlay(frame: &mut Frame<'_>, area: Rect, screen: ActiveScreen) Line::from(" w - toggle timeframe (Week/Month)"), Line::from(" f - toggle layout (Horz/Vert)"), Line::from(" n - cycle display style (Classic/System Compact/Full)"), - Line::from(" Mouse - click tabs/controls; footer cycles style"), + Line::from(" Mouse - click tabs/controls/format/quit"), Line::from(" r/F5 - refresh usage + limits"), Line::from(" s/F2 - switch screen"), Line::from(" ? - toggle help"), - Line::from(" q/Esc - quit"), + Line::from(" q - quit (confirm)"), ]), ActiveScreen::Activity => Text::from(vec![ Line::from("Keys:"), @@ -2467,26 +2506,26 @@ fn render_help_overlay(frame: &mut Frame<'_>, area: Rect, screen: ActiveScreen) Line::from(" +/= - show more projects"), Line::from(" - - show fewer projects"), Line::from(" n - cycle display style (Classic/System Compact/Full)"), - Line::from(" Mouse - click tabs/view/projects; footer cycles style"), + Line::from(" Mouse - click tabs/view/projects/quit"), Line::from(" r/F5 - refresh usage + limits"), Line::from(" s/F2 - switch screen"), Line::from(" ? - toggle help"), - Line::from(" q/Esc - quit"), + Line::from(" q - quit (confirm)"), ]), ActiveScreen::LimitResets => Text::from(vec![ Line::from("Keys:"), Line::from(" r/F5 - refresh reset credits"), Line::from(" n - cycle display style (Classic/System Compact/Full)"), - Line::from(" Mouse - click tabs; footer cycles style"), + Line::from(" Mouse - click tabs/quit"), Line::from(" s/F2 - switch screen"), Line::from(" ? - toggle help"), - Line::from(" q/Esc - quit"), + Line::from(" q - quit (confirm)"), ]), ActiveScreen::Read => Text::from(vec![ Line::from("Keys:"), Line::from(" n - cycle display style (Classic/System Compact/Full)"), - Line::from(" Mouse - click tabs; footer cycles style"), - Line::from(" q/Esc - quit"), + Line::from(" Mouse - click tabs/quit"), + Line::from(" q - quit (confirm)"), ]), }; frame.render_widget( @@ -2551,6 +2590,65 @@ fn render_no_sessions_overlay(frame: &mut Frame<'_>, area: Rect, state: &AppStat ); } +fn render_quit_confirmation(frame: &mut Frame<'_>, area: Rect, state: &mut AppState) { + state.ui_hit_targets.clear(); + + let popup = centered_rect(area.width.min(35), area.height.min(7), area); + frame.render_widget(Clear, popup); + frame.render_widget( + Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Plain) + .title(Span::styled( + " Quit ", + Style::default().add_modifier(Modifier::BOLD), + )), + popup, + ); + + let message_area = Rect::new( + popup.x.saturating_add(1), + popup.y.saturating_add(2), + popup.width.saturating_sub(2), + 2.min(popup.height.saturating_sub(3)), + ); + frame.render_widget( + Paragraph::new(Text::from(vec![ + Line::from("Are you sure you want to quit?"), + Line::from(Span::styled( + "Y confirms; N/Esc/Enter cancels", + Style::default().fg(Color::Gray), + )), + ])) + .alignment(Alignment::Center), + message_area, + ); + + let buttons_area = Rect::new( + popup.x.saturating_add(1), + popup.y.saturating_add(popup.height.saturating_sub(2)), + popup.width.saturating_sub(2), + 1, + ); + let segments = [ + (" YES ", Some(UiClickAction::ConfirmQuit)), + (" ", None), + (" NO ", Some(UiClickAction::CancelQuit)), + ]; + state + .ui_hit_targets + .extend(centered_targets(buttons_area, &segments)); + frame.render_widget( + Paragraph::new(Line::from(vec![ + pill("YES", false), + Span::raw(" "), + pill("NO", true), + ])) + .alignment(Alignment::Center), + buttons_area, + ); +} + fn pill(label: &str, active: bool) -> Span<'static> { let style = if active { Style::default() @@ -2563,12 +2661,6 @@ fn pill(label: &str, active: bool) -> Span<'static> { Span::styled(format!(" {label} "), style) } -fn register_hit_target(state: &mut AppState, area: Rect, action: UiClickAction) { - if area.width > 0 && area.height > 0 { - state.ui_hit_targets.push(UiHitTarget { area, action }); - } -} - fn register_right_aligned_targets( state: &mut AppState, area: Rect, @@ -2612,6 +2704,33 @@ fn right_aligned_targets( targets } +fn centered_targets(area: Rect, segments: &[(&str, Option)]) -> Vec { + let total_width = segments.iter().fold(0_u16, |total, (text, _)| { + total.saturating_add(UnicodeWidthStr::width(*text).min(u16::MAX as usize) as u16) + }); + if total_width > area.width || area.height == 0 { + return Vec::new(); + } + + let mut targets = Vec::new(); + let mut x = area + .x + .saturating_add((area.width.saturating_sub(total_width)) / 2); + for (text, action) in segments { + let width = UnicodeWidthStr::width(*text).min(u16::MAX as usize) as u16; + if let Some(action) = action { + if width > 0 { + targets.push(UiHitTarget { + area: Rect::new(x, area.y, width, 1), + action: *action, + }); + } + } + x = x.saturating_add(width); + } + targets +} + fn card( title: &str, value: &str, @@ -2621,6 +2740,10 @@ fn card( card_with_captions(title, value, &[caption1, caption2]) } +fn today_card_title(formatter: DisplayFormatter<'_>, now: NaiveDateTime) -> String { + format!("TODAY {}", formatter.format_session_datetime(now)) +} + fn card4( title: &str, value: &str, @@ -3371,6 +3494,18 @@ mod tests { assert_eq!(targets[3].area, Rect::new(54, 2, 9, 1)); } + #[test] + fn today_card_title_includes_the_current_date_and_time() { + let system_locale = crate::locale::SystemLocale::default(); + let formatter = DisplayFormatter::new(DisplayStyle::Classic, &system_locale); + let now = NaiveDate::from_ymd_opt(2026, 7, 22) + .unwrap() + .and_hms_opt(14, 35, 0) + .unwrap(); + + assert_eq!(today_card_title(formatter, now), "TODAY 2026-07-22 14:35"); + } + #[test] fn navigation_title_hides_tabs_before_they_touch_the_version() { let (title, targets) = navigation_title(Rect::new(0, 0, 52, 10), ActiveScreen::Usage); @@ -3378,6 +3513,33 @@ mod tests { assert!(targets.is_empty()); } + #[test] + fn quit_action_is_on_the_bottom_right_border() { + let (title, targets) = quit_title(Rect::new(4, 2, 71, 20), true); + + assert_eq!(title.spans[0].style.fg, Some(Color::Black)); + assert_eq!(title.spans[0].style.bg, Some(Color::White)); + assert_eq!(targets.len(), 1); + assert_eq!(targets[0].area, Rect::new(67, 21, 6, 1)); + assert_eq!(targets[0].action, UiClickAction::PromptQuit); + } + + #[test] + fn quit_confirmation_buttons_are_centered_and_separate() { + let targets = centered_targets( + Rect::new(10, 5, 33, 1), + &[ + (" YES ", Some(UiClickAction::ConfirmQuit)), + (" ", None), + (" NO ", Some(UiClickAction::CancelQuit)), + ], + ); + + assert_eq!(targets.len(), 2); + assert_eq!(targets[0].area, Rect::new(20, 5, 5, 1)); + assert_eq!(targets[1].area, Rect::new(28, 5, 4, 1)); + } + #[test] fn usage_header_has_one_blank_row_above_and_below() { let chunks = usage_screen_layout(Rect::new(2, 1, 100, 20), 1); @@ -3686,11 +3848,11 @@ mod tests { u16::MAX, formatter, ), - "45456785 / 1756241" + "45 456 785 / 1 756 241" ); assert_eq!( format_vertical_token_value(45_456_785, 20, formatter), - "45456785" + "45 456 785" ); assert_eq!( format_horizontal_value( @@ -3700,11 +3862,11 @@ mod tests { 8, formatter, ), - "45456785 / 1756241" + "45 456 785 / 1 756 241" ); assert_eq!( format_vertical_token_value(45_456_785, 4, formatter), - "45456785" + "45 456 785" ); } From 928ca9f5b01cae7dd6404821c0dbcd031dd43521 Mon Sep 17 00:00:00 2001 From: woffko <2505149+woffko@users.noreply.github.com> Date: Wed, 22 Jul 2026 21:26:40 +0300 Subject: [PATCH 2/4] Release 0.4.3 with chart hover and quit preferences --- CHANGELOG.md | 4 + Cargo.lock | 2 +- Cargo.toml | 2 +- README.md | 3 + src/app/mod.rs | 132 +++++++++++++++++- src/ui/mod.rs | 363 ++++++++++++++++++++++++++++++++++++++++++++++--- 6 files changed, 483 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index edaabd9..45c98b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ All notable changes to this project are documented in this file. ## Unreleased +## 0.4.3 - 2026-07-22 + - Added a persisted `n` shortcut to cycle display formatting through Classic, System Compact, and System Full for numbers, dates, times, and calendar labels. - Clarified token pair charts with the `TOKENS (TOTAL / NON-CACHED)` heading. - Added compact `K/M/B/T` notation for large dashboard token values in System Compact mode, including values inside vertical chart bars. System Full keeps those values expanded, while Classic output remains unchanged. @@ -14,6 +16,8 @@ All notable changes to this project are documented in this file. - System Full now groups expanded integers with regular spaces while retaining system-localized dates, times, and decimals. - Added a compact `STYLE CLASS/SCOMP/SFULL` selector after the Usage bar controls and a bottom-border Quit action with a safe Yes/No confirmation dialog; the `q` shortcut opens the same dialog. - Added the current locale-aware date and time to the `TODAY` card border. +- Vertical chart values now compact only when an individual bar is too narrow, with an exact locale-aware value tooltip on mouse hover. +- Exit confirmation can be disabled from the quit dialog, persists in `state.json`, and can be re-enabled through the confirmed checkbox beside `QUIT`. ## 0.3.6 - 2026-06-05 diff --git a/Cargo.lock b/Cargo.lock index d94ad9a..f29e785 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -259,7 +259,7 @@ checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] name = "comon" -version = "0.4.2" +version = "0.4.3" dependencies = [ "anyhow", "chrono", diff --git a/Cargo.toml b/Cargo.toml index 1fd2d99..015db58 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "comon" -version = "0.4.2" +version = "0.4.3" edition = "2021" license = "Apache-2.0" diff --git a/README.md b/README.md index 36dc425..7435250 100644 --- a/README.md +++ b/README.md @@ -134,6 +134,7 @@ comon --scan-time-budget-ms 1500 --max-jsonl-line-kib 512 - `f` Toggle layout (Horz/Vert) - `n` Cycle display formatting (Classic/System Compact/System Full) - Mouse: click the top tabs, Usage/Activity controls (including the Usage style selector), or the bottom-right Quit action +- Mouse: hover a filled vertical chart bar to see its exact date and full locale-aware value - `s` / `F2` Switch between Usage and Session history - `r` / `F5` Refresh current screen - `?` Help overlay @@ -343,6 +344,8 @@ CI also runs this check on each push and pull request via `.github/workflows/asc - Limits/credits require Codex App Server to start successfully (auth, environment, and a usable working directory). CoMon auto-detects `codex` on `PATH` and common Windows Codex App bundle locations; use `--codex-bin` or `--app-server-bin` when needed. - comon stores local app state in `~/.comon/state.json` by default (or `$COMON_HOME`, or `--comon-home`). - Display formatting starts in Classic mode; press `n` or use `STYLE CLASS/SCOMP/SFULL` in the Usage controls to choose Classic, System Compact, or System Full. Both System modes use the operating system locale for dates, times, decimals, and calendar labels; Compact uses the detected thousands separator and abbreviates dashboard token values, while Full groups expanded integers with regular spaces. The choice is saved in `state.json` without changing stored data. +- Vertical chart labels preserve the selected style when they fit and compact only individual values that exceed their bar width. Hovering a filled bar shows the exact value. +- The quit dialog's `Don't show again` checkbox disables future `q`/`QUIT` confirmations after a confirmed exit. The checkbox beside `QUIT` shows that saved state; clicking it asks before enabling or disabling confirmation. - comon stores scan cache in `~/.comon/comon.db` to avoid rereading unchanged session files. - Large session logs are parsed incrementally with persisted parser offsets in `comon.db`; unchanged files are reused from cache. - If historical days look incomplete after adding old session files, run once with `--full-scan --scan-time-budget-ms 0` to force a full reparse and refresh cached summaries. diff --git a/src/app/mod.rs b/src/app/mod.rs index 173cfc8..9c7a56e 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -83,6 +83,10 @@ pub(crate) enum UiClickAction { PromptQuit, CancelQuit, ConfirmQuit, + ToggleQuitDontAskAgain, + PromptQuitConfirmationPreference, + ConfirmQuitConfirmationPreference, + CancelQuitConfirmationPreference, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -142,8 +146,12 @@ pub(crate) struct AppState { pub(crate) no_sessions_confirm_open: bool, pub(crate) no_sessions_confirm_dismissed: bool, pub(crate) quit_confirm_open: bool, + pub(crate) quit_dont_ask_again: bool, + pub(crate) skip_quit_confirmation: bool, + pub(crate) quit_preference_prompt: Option, pub(crate) display_style: DisplayStyle, pub(crate) system_locale: SystemLocale, + pub(crate) mouse_position: Option<(u16, u16)>, pub(crate) ui_hit_targets: Vec, pub(crate) usage: Option, @@ -181,6 +189,7 @@ struct PersistedUiState { workspace_path: Option, no_sessions_confirm_dismissed: bool, display_style: DisplayStyle, + skip_quit_confirmation: bool, } impl PersistedUiState { @@ -193,6 +202,7 @@ impl PersistedUiState { workspace_path, no_sessions_confirm_dismissed: false, display_style: DisplayStyle::Classic, + skip_quit_confirmation: false, } } @@ -205,6 +215,7 @@ impl PersistedUiState { workspace_path: state.workspace_path.clone(), no_sessions_confirm_dismissed: state.no_sessions_confirm_dismissed, display_style: state.display_style, + skip_quit_confirmation: state.skip_quit_confirmation, } } } @@ -235,6 +246,8 @@ struct StoredGlobalState { orientation: Option, activity_project_limit: Option, display_style: Option, + #[serde(default)] + skip_quit_confirmation: bool, last_workspace_path: Option, updated_at: i64, } @@ -484,8 +497,12 @@ async fn run_inner( no_sessions_confirm_open: false, no_sessions_confirm_dismissed: restored_ui_state.no_sessions_confirm_dismissed, quit_confirm_open: false, + quit_dont_ask_again: false, + skip_quit_confirmation: restored_ui_state.skip_quit_confirmation, + quit_preference_prompt: None, display_style: restored_ui_state.display_style, system_locale: config.system_locale.clone(), + mouse_position: None, ui_hit_targets: Vec::new(), usage: None, usage_updated_at: None, @@ -608,21 +625,75 @@ fn handle_input_event( usage_refresh_tx: &mpsc::Sender<()>, limits_refresh_tx: &mpsc::Sender<()>, ) -> Result { + if let Some(desired_skip_confirmation) = state.quit_preference_prompt { + return match event { + Event::Mouse(mouse) if mouse.kind == MouseEventKind::Down(MouseButton::Left) => { + match ui_click_action_at(&state.ui_hit_targets, mouse.column, mouse.row) { + Some(UiClickAction::ConfirmQuitConfirmationPreference) => { + state.skip_quit_confirmation = desired_skip_confirmation; + state.quit_preference_prompt = None; + Ok(InputOutcome::Continue(true)) + } + Some(UiClickAction::CancelQuitConfirmationPreference) => { + state.quit_preference_prompt = None; + Ok(InputOutcome::Continue(true)) + } + _ => Ok(InputOutcome::Continue(false)), + } + } + Event::Key(key) if key.kind == KeyEventKind::Press => match (key.code, key.modifiers) { + (KeyCode::Char('y'), _) | (KeyCode::Char('Y'), _) => { + state.skip_quit_confirmation = desired_skip_confirmation; + state.quit_preference_prompt = None; + Ok(InputOutcome::Continue(true)) + } + (KeyCode::Char('c'), KeyModifiers::CONTROL) => Ok(InputOutcome::Quit), + (KeyCode::Enter, _) + | (KeyCode::Esc, _) + | (KeyCode::Char('n'), _) + | (KeyCode::Char('N'), _) + | (KeyCode::Char('q'), _) + | (KeyCode::Char('Q'), _) => { + state.quit_preference_prompt = None; + Ok(InputOutcome::Continue(true)) + } + _ => Ok(InputOutcome::Continue(false)), + }, + Event::Resize(_, _) => Ok(InputOutcome::Continue(true)), + _ => Ok(InputOutcome::Continue(false)), + }; + } + if state.quit_confirm_open { return match event { Event::Mouse(mouse) if mouse.kind == MouseEventKind::Down(MouseButton::Left) => { match ui_click_action_at(&state.ui_hit_targets, mouse.column, mouse.row) { - Some(UiClickAction::ConfirmQuit) => Ok(InputOutcome::Quit), + Some(UiClickAction::ConfirmQuit) => { + state.skip_quit_confirmation = state.quit_dont_ask_again; + Ok(InputOutcome::Quit) + } Some(UiClickAction::CancelQuit) => { state.quit_confirm_open = false; + state.quit_dont_ask_again = false; + Ok(InputOutcome::Continue(true)) + } + Some(UiClickAction::ToggleQuitDontAskAgain) => { + state.quit_dont_ask_again = !state.quit_dont_ask_again; Ok(InputOutcome::Continue(true)) } _ => Ok(InputOutcome::Continue(false)), } } Event::Key(key) if key.kind == KeyEventKind::Press => match (key.code, key.modifiers) { - (KeyCode::Char('y'), _) | (KeyCode::Char('Y'), _) => Ok(InputOutcome::Quit), + (KeyCode::Char('y'), _) | (KeyCode::Char('Y'), _) => { + state.skip_quit_confirmation = state.quit_dont_ask_again; + Ok(InputOutcome::Quit) + } (KeyCode::Char('c'), KeyModifiers::CONTROL) => Ok(InputOutcome::Quit), + (KeyCode::Char(' '), _) => { + state.quit_dont_ask_again = !state.quit_dont_ask_again; + Ok(InputOutcome::Continue(true)) + } (KeyCode::Enter, _) | (KeyCode::Esc, _) | (KeyCode::Char('n'), _) @@ -630,6 +701,7 @@ fn handle_input_event( | (KeyCode::Char('q'), _) | (KeyCode::Char('Q'), _) => { state.quit_confirm_open = false; + state.quit_dont_ask_again = false; Ok(InputOutcome::Continue(true)) } _ => Ok(InputOutcome::Continue(false)), @@ -640,19 +712,28 @@ fn handle_input_event( } if let Event::Mouse(mouse) = &event { + let mouse_position = Some((mouse.column, mouse.row)); + let mouse_moved = state.mouse_position != mouse_position; + state.mouse_position = mouse_position; + if state.show_help || state.no_sessions_confirm_open { return Ok(InputOutcome::Continue(false)); } if mouse.kind == MouseEventKind::Down(MouseButton::Left) { if let Some(action) = ui_click_action_at(&state.ui_hit_targets, mouse.column, mouse.row) { - if action == UiClickAction::ConfirmQuit { + if action == UiClickAction::ConfirmQuit + || (action == UiClickAction::PromptQuit && state.skip_quit_confirmation) + { return Ok(InputOutcome::Quit); } let changed = apply_ui_click_action(state, action); return Ok(InputOutcome::Continue(changed)); } } + if mouse.kind == MouseEventKind::Moved { + return Ok(InputOutcome::Continue(mouse_moved)); + } } if let Event::Key(key) = &event { @@ -662,7 +743,11 @@ fn handle_input_event( match (key.code, key.modifiers) { (KeyCode::Char('q'), _) | (KeyCode::Char('Q'), _) => { + if state.skip_quit_confirmation { + return Ok(InputOutcome::Quit); + } state.quit_confirm_open = true; + state.quit_dont_ask_again = false; return Ok(InputOutcome::Continue(true)); } (KeyCode::Char('c'), KeyModifiers::CONTROL) => return Ok(InputOutcome::Quit), @@ -688,6 +773,7 @@ fn handle_input_event( } if matches!(event, Event::Resize(_, _)) { + state.mouse_position = None; return Ok(InputOutcome::Continue(true)); } @@ -775,13 +861,36 @@ fn apply_ui_click_action(state: &mut AppState, action: UiClickAction) -> bool { } UiClickAction::PromptQuit => { state.quit_confirm_open = true; + state.quit_dont_ask_again = false; true } UiClickAction::CancelQuit => { state.quit_confirm_open = false; + state.quit_dont_ask_again = false; + true + } + UiClickAction::ConfirmQuit => { + state.skip_quit_confirmation = state.quit_dont_ask_again; + false + } + UiClickAction::ToggleQuitDontAskAgain => { + state.quit_dont_ask_again = !state.quit_dont_ask_again; + true + } + UiClickAction::PromptQuitConfirmationPreference => { + state.quit_preference_prompt = Some(!state.skip_quit_confirmation); + true + } + UiClickAction::ConfirmQuitConfirmationPreference => { + if let Some(desired_skip_confirmation) = state.quit_preference_prompt.take() { + state.skip_quit_confirmation = desired_skip_confirmation; + } + true + } + UiClickAction::CancelQuitConfirmationPreference => { + state.quit_preference_prompt = None; true } - UiClickAction::ConfirmQuit => false, } } @@ -1049,6 +1158,7 @@ fn load_persisted_ui_state( state.display_style = style; } } + state.skip_quit_confirmation = store.global.skip_quit_confirmation; if let Some(workspace_path) = state.workspace_path.as_ref() { let workspace_key = workspace_path.to_string_lossy(); @@ -1077,6 +1187,7 @@ fn save_persisted_ui_state(comon_home: &Path, state: &PersistedUiState) -> Resul .clamp(MIN_ACTIVITY_PROJECT_LIMIT, MAX_ACTIVITY_PROJECT_LIMIT), ); store.global.display_style = Some(state.display_style.store_value().to_string()); + store.global.skip_quit_confirmation = state.skip_quit_confirmation; store.global.last_workspace_path = workspace_path_text.clone(); store.global.updated_at = now; @@ -1311,6 +1422,19 @@ mod tests { let _ = std::fs::remove_dir_all(comon_home); } + #[test] + fn quit_confirmation_preference_round_trips_through_state_store() { + let comon_home = make_temp_dir("quit-confirmation-preference"); + let mut state = PersistedUiState::default_for_workspace(None); + state.skip_quit_confirmation = true; + + save_persisted_ui_state(&comon_home, &state).expect("save persisted ui state"); + let loaded = load_persisted_ui_state(&comon_home, None).expect("load persisted ui state"); + assert!(loaded.skip_quit_confirmation); + + let _ = std::fs::remove_dir_all(comon_home); + } + #[test] fn legacy_system_style_restores_as_system_compact() { let comon_home = make_temp_dir("legacy-system-display-style"); diff --git a/src/ui/mod.rs b/src/ui/mod.rs index acc5c66..8df63f8 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -78,7 +78,8 @@ pub fn render(frame: &mut Frame<'_>, state: &mut AppState) { let area = frame.area(); let (navigation, navigation_targets) = navigation_title(area, state.active_screen); state.ui_hit_targets.extend(navigation_targets); - let (quit, quit_targets) = quit_title(area, state.quit_confirm_open); + let (quit, quit_targets) = + quit_title(area, state.quit_confirm_open, state.skip_quit_confirmation); state.ui_hit_targets.extend(quit_targets); let outer = Block::default() @@ -163,6 +164,8 @@ pub fn render(frame: &mut Frame<'_>, state: &mut AppState) { if state.quit_confirm_open { render_quit_confirmation(frame, area, state); + } else if state.quit_preference_prompt.is_some() { + render_quit_preference_confirmation(frame, area, state); } } @@ -228,15 +231,35 @@ fn navigation_title(area: Rect, active_screen: ActiveScreen) -> (Line<'static>, (line, right_aligned_targets(title_area, &segments)) } -fn quit_title(area: Rect, active: bool) -> (Line<'static>, Vec) { +fn quit_title( + area: Rect, + active: bool, + skip_confirmation: bool, +) -> (Line<'static>, Vec) { let title_area = Rect::new( area.x.saturating_add(1), area.y.saturating_add(area.height.saturating_sub(1)), area.width.saturating_sub(2), 1, ); - let segments = [(" QUIT ", Some(UiClickAction::PromptQuit)), (" ", None)]; - let line = Line::from(vec![pill("QUIT", active), Span::raw(" ")]); + let checkbox = if skip_confirmation { + " [✓] " + } else { + " [ ] " + }; + let segments = [ + ( + checkbox, + Some(UiClickAction::PromptQuitConfirmationPreference), + ), + (" QUIT ", Some(UiClickAction::PromptQuit)), + (" ", None), + ]; + let line = Line::from(vec![ + checkbox_span(skip_confirmation, None), + pill("QUIT", active), + Span::raw(" "), + ]); (line, right_aligned_targets(title_area, &segments)) } @@ -2021,6 +2044,27 @@ fn render_usage_chart(frame: &mut Frame<'_>, area: Rect, state: &AppState) { let bw = bar_width.max(1); let max_value = values.iter().copied().max().unwrap_or(0).max(1); + let hovered_bar = hovered_vertical_bar_index( + state.mouse_position, + bars_area, + bw, + bar_gap, + &values, + max_value, + ); + let hover_tooltip = hovered_bar.and_then(|index| { + state.mouse_position.map(|mouse| { + ( + mouse, + format_vertical_bar_tooltip( + &days[index].day, + values[index], + state.metric, + formatter, + ), + ) + }) + }); let buf = frame.buffer_mut(); // Bars are laid left-to-right. @@ -2084,7 +2128,7 @@ fn render_usage_chart(frame: &mut Frame<'_>, area: Rect, state: &AppState) { } } UsageMetric::Runs => { - let raw = value.to_string(); + let raw = formatter.format_u64(*value); if raw.len() <= w as usize { raw } else { @@ -2123,6 +2167,10 @@ fn render_usage_chart(frame: &mut Frame<'_>, area: Rect, state: &AppState) { } } } + + if let Some((mouse, tooltip)) = hover_tooltip { + render_chart_tooltip(frame, inner, mouse, &tooltip); + } } ChartOrientation::Horizontal => { // Multi-line horizontal bars, values outside to the right. @@ -2325,6 +2373,126 @@ fn usage_chart_metric_label(metric: UsageMetric) -> &'static str { } } +fn hovered_vertical_bar_index( + mouse_position: Option<(u16, u16)>, + bars_area: Rect, + bar_width: u16, + bar_gap: u16, + values: &[u64], + max_value: u64, +) -> Option { + let (mouse_x, mouse_y) = mouse_position?; + let right = bars_area.x.saturating_add(bars_area.width); + let bottom = bars_area.y.saturating_add(bars_area.height); + if mouse_x < bars_area.x + || mouse_x >= right + || mouse_y < bars_area.y + || mouse_y >= bottom + || bar_width == 0 + { + return None; + } + + let stride = bar_width.saturating_add(bar_gap); + if stride == 0 { + return None; + } + let offset_x = mouse_x.saturating_sub(bars_area.x); + let index = (offset_x / stride) as usize; + let value = *values.get(index)?; + let bar_x = bars_area + .x + .saturating_add((index as u16).saturating_mul(stride)); + let actual_width = bar_width.min(right.saturating_sub(bar_x)); + if mouse_x >= bar_x.saturating_add(actual_width) { + return None; + } + + let ratio = (value as f64) / (max_value.max(1) as f64); + let filled_height = ((bars_area.height as f64) * ratio.clamp(0.0, 1.0)).round() as u16; + if filled_height == 0 { + return None; + } + let bottom_y = bottom.saturating_sub(1); + let top_filled_y = bottom_y.saturating_sub(filled_height.saturating_sub(1)); + (mouse_y >= top_filled_y).then_some(index) +} + +fn format_vertical_bar_tooltip( + day: &str, + value: u64, + metric: UsageMetric, + formatter: DisplayFormatter<'_>, +) -> String { + let date = NaiveDate::parse_from_str(day, "%Y-%m-%d") + .map(|date| formatter.format_full_date(date)) + .unwrap_or_else(|_| day.to_string()); + let value = match metric { + UsageMetric::Tokens => format!("{} tokens", formatter.format_u64(value)), + UsageMetric::Time => format_duration_words( + value.min((i64::MAX as u64) / 60_000).saturating_mul(60_000) as i64, + ), + UsageMetric::Runs => format!("{} runs", formatter.format_u64(value)), + }; + format!("{date} · {value}") +} + +fn render_chart_tooltip(frame: &mut Frame<'_>, bounds: Rect, mouse: (u16, u16), text: &str) { + let Some(area) = chart_tooltip_rect( + bounds, + mouse, + UnicodeWidthStr::width(text).min(u16::MAX as usize) as u16, + ) else { + return; + }; + let text = truncate_middle(text, area.width.saturating_sub(2) as usize); + let block = Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Plain) + .border_style(Style::default().fg(Color::White)) + .style(Style::default().bg(Color::Black)); + frame.render_widget(Clear, area); + frame.render_widget( + Paragraph::new(Line::from(Span::styled( + text, + Style::default().add_modifier(Modifier::BOLD), + ))) + .alignment(Alignment::Center) + .block(block), + area, + ); +} + +fn chart_tooltip_rect(bounds: Rect, mouse: (u16, u16), content_width: u16) -> Option { + if bounds.width < 3 || bounds.height < 3 { + return None; + } + let width = content_width.saturating_add(2).clamp(3, bounds.width); + let height = 3; + let right = bounds.x.saturating_add(bounds.width); + let bottom = bounds.y.saturating_add(bounds.height); + let max_x = right.saturating_sub(width); + let max_y = bottom.saturating_sub(height); + + let preferred_x = if mouse.0.saturating_add(1).saturating_add(width) <= right { + mouse.0.saturating_add(1) + } else { + mouse.0.saturating_sub(width) + }; + let preferred_y = if mouse.1 >= bounds.y.saturating_add(height) { + mouse.1.saturating_sub(height) + } else { + mouse.1.saturating_add(1) + }; + + Some(Rect::new( + preferred_x.clamp(bounds.x, max_x), + preferred_y.clamp(bounds.y, max_y), + width, + height, + )) +} + fn format_vertical_token_value( value: u64, max_width: u16, @@ -2335,7 +2503,7 @@ fn format_vertical_token_value( DisplayStyle::SystemCompact => format_tokens_compact(value as i64, formatter), DisplayStyle::SystemFull => formatter.format_u64(value), }; - if preferred.len() <= max_width as usize || formatter.style() == DisplayStyle::SystemFull { + if preferred.len() <= max_width as usize { preferred } else { format_compact_kmb(value, max_width, formatter) @@ -2593,7 +2761,7 @@ fn render_no_sessions_overlay(frame: &mut Frame<'_>, area: Rect, state: &AppStat fn render_quit_confirmation(frame: &mut Frame<'_>, area: Rect, state: &mut AppState) { state.ui_hit_targets.clear(); - let popup = centered_rect(area.width.min(35), area.height.min(7), area); + let popup = centered_rect(area.width.min(39), area.height.min(10), area); frame.render_widget(Clear, popup); frame.render_widget( Block::default() @@ -2610,7 +2778,7 @@ fn render_quit_confirmation(frame: &mut Frame<'_>, area: Rect, state: &mut AppSt popup.x.saturating_add(1), popup.y.saturating_add(2), popup.width.saturating_sub(2), - 2.min(popup.height.saturating_sub(3)), + 3.min(popup.height.saturating_sub(3)), ); frame.render_widget( Paragraph::new(Text::from(vec![ @@ -2619,11 +2787,39 @@ fn render_quit_confirmation(frame: &mut Frame<'_>, area: Rect, state: &mut AppSt "Y confirms; N/Esc/Enter cancels", Style::default().fg(Color::Gray), )), + Line::from(Span::styled( + "Space toggles the checkbox", + Style::default().fg(Color::Gray), + )), ])) .alignment(Alignment::Center), message_area, ); + let checkbox_area = Rect::new( + popup.x.saturating_add(1), + popup.y.saturating_add(6), + popup.width.saturating_sub(2), + 1, + ); + let checkbox_text = if state.quit_dont_ask_again { + "[✓] Don't show again" + } else { + "[ ] Don't show again" + }; + state.ui_hit_targets.extend(centered_targets( + checkbox_area, + &[(checkbox_text, Some(UiClickAction::ToggleQuitDontAskAgain))], + )); + frame.render_widget( + Paragraph::new(Line::from(checkbox_span( + state.quit_dont_ask_again, + Some("Don't show again"), + ))) + .alignment(Alignment::Center), + checkbox_area, + ); + let buttons_area = Rect::new( popup.x.saturating_add(1), popup.y.saturating_add(popup.height.saturating_sub(2)), @@ -2649,6 +2845,98 @@ fn render_quit_confirmation(frame: &mut Frame<'_>, area: Rect, state: &mut AppSt ); } +fn render_quit_preference_confirmation(frame: &mut Frame<'_>, area: Rect, state: &mut AppState) { + state.ui_hit_targets.clear(); + let disable_confirmation = state.quit_preference_prompt.unwrap_or(false); + let (title, question, explanation) = if disable_confirmation { + ( + " Exit confirmation ", + "Disable exit confirmation?", + "q and QUIT will exit immediately.", + ) + } else { + ( + " Exit confirmation ", + "Enable exit confirmation?", + "q and QUIT will ask before exiting.", + ) + }; + + let popup = centered_rect(area.width.min(43), area.height.min(8), area); + frame.render_widget(Clear, popup); + frame.render_widget( + Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Plain) + .title(Span::styled( + title, + Style::default().add_modifier(Modifier::BOLD), + )), + popup, + ); + + let message_area = Rect::new( + popup.x.saturating_add(1), + popup.y.saturating_add(2), + popup.width.saturating_sub(2), + 2.min(popup.height.saturating_sub(3)), + ); + frame.render_widget( + Paragraph::new(Text::from(vec![ + Line::from(question), + Line::from(Span::styled(explanation, Style::default().fg(Color::Gray))), + ])) + .alignment(Alignment::Center), + message_area, + ); + + let buttons_area = Rect::new( + popup.x.saturating_add(1), + popup.y.saturating_add(popup.height.saturating_sub(2)), + popup.width.saturating_sub(2), + 1, + ); + let segments = [ + ( + " YES ", + Some(UiClickAction::ConfirmQuitConfirmationPreference), + ), + (" ", None), + ( + " NO ", + Some(UiClickAction::CancelQuitConfirmationPreference), + ), + ]; + state + .ui_hit_targets + .extend(centered_targets(buttons_area, &segments)); + frame.render_widget( + Paragraph::new(Line::from(vec![ + pill("YES", false), + Span::raw(" "), + pill("NO", true), + ])) + .alignment(Alignment::Center), + buttons_area, + ); +} + +fn checkbox_span(checked: bool, label: Option<&str>) -> Span<'static> { + let mark = if checked { "✓" } else { " " }; + let text = match label { + Some(label) => format!("[{mark}] {label}"), + None => format!(" [{mark}] "), + }; + let style = if checked { + Style::default() + .fg(Color::White) + .add_modifier(Modifier::BOLD) + } else { + Style::default().fg(Color::Gray) + }; + Span::styled(text, style) +} + fn pill(label: &str, active: bool) -> Span<'static> { let style = if active { Style::default() @@ -3515,13 +3803,19 @@ mod tests { #[test] fn quit_action_is_on_the_bottom_right_border() { - let (title, targets) = quit_title(Rect::new(4, 2, 71, 20), true); + let (title, targets) = quit_title(Rect::new(4, 2, 71, 20), true, true); - assert_eq!(title.spans[0].style.fg, Some(Color::Black)); - assert_eq!(title.spans[0].style.bg, Some(Color::White)); - assert_eq!(targets.len(), 1); - assert_eq!(targets[0].area, Rect::new(67, 21, 6, 1)); - assert_eq!(targets[0].action, UiClickAction::PromptQuit); + assert_eq!(title.spans[0].content.as_ref(), " [✓] "); + assert_eq!(title.spans[1].style.fg, Some(Color::Black)); + assert_eq!(title.spans[1].style.bg, Some(Color::White)); + assert_eq!(targets.len(), 2); + assert_eq!(targets[0].area, Rect::new(62, 21, 5, 1)); + assert_eq!( + targets[0].action, + UiClickAction::PromptQuitConfirmationPreference + ); + assert_eq!(targets[1].area, Rect::new(67, 21, 6, 1)); + assert_eq!(targets[1].action, UiClickAction::PromptQuit); } #[test] @@ -3835,7 +4129,7 @@ mod tests { } #[test] - fn system_full_keeps_chart_token_values_expanded() { + fn system_full_compacts_only_tight_vertical_token_values() { let system_locale = crate::locale::SystemLocale::default(); let formatter = DisplayFormatter::new(crate::locale::DisplayStyle::SystemFull, &system_locale); @@ -3864,9 +4158,44 @@ mod tests { ), "45 456 785 / 1 756 241" ); + assert_eq!(format_vertical_token_value(45_456_785, 4, formatter), "45M"); + } + + #[test] + fn vertical_bar_hover_requires_the_filled_bar_and_ignores_gaps() { + let area = Rect::new(10, 5, 11, 10); + let values = [50, 100]; + assert_eq!( - format_vertical_token_value(45_456_785, 4, formatter), - "45 456 785" + hovered_vertical_bar_index(Some((10, 10)), area, 3, 1, &values, 100), + Some(0) + ); + assert_eq!( + hovered_vertical_bar_index(Some((10, 9)), area, 3, 1, &values, 100), + None + ); + assert_eq!( + hovered_vertical_bar_index(Some((13, 12)), area, 3, 1, &values, 100), + None + ); + assert_eq!( + hovered_vertical_bar_index(Some((14, 5)), area, 3, 1, &values, 100), + Some(1) + ); + } + + #[test] + fn vertical_bar_tooltip_shows_exact_value_and_stays_inside_chart() { + let system_locale = crate::locale::SystemLocale::default(); + let formatter = DisplayFormatter::new(DisplayStyle::Classic, &system_locale); + + assert_eq!( + format_vertical_bar_tooltip("2026-07-22", 45_456_785, UsageMetric::Tokens, formatter,), + "2026-07-22 · 45,456,785 tokens" + ); + assert_eq!( + chart_tooltip_rect(Rect::new(2, 2, 20, 10), (20, 3), 10), + Some(Rect::new(8, 4, 12, 3)) ); } From 4e344a0f5b8088fe80ea420bedfac7dbcadd23e1 Mon Sep 17 00:00:00 2001 From: woffko <2505149+woffko@users.noreply.github.com> Date: Wed, 22 Jul 2026 21:35:33 +0300 Subject: [PATCH 3/4] Fix ASCII-only UI labels --- src/ui/mod.rs | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 8df63f8..ca3f33f 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -242,11 +242,7 @@ fn quit_title( area.width.saturating_sub(2), 1, ); - let checkbox = if skip_confirmation { - " [✓] " - } else { - " [ ] " - }; + let checkbox = if skip_confirmation { " [x] " } else { " [ ] " }; let segments = [ ( checkbox, @@ -2434,7 +2430,7 @@ fn format_vertical_bar_tooltip( ), UsageMetric::Runs => format!("{} runs", formatter.format_u64(value)), }; - format!("{date} · {value}") + format!("{date} | {value}") } fn render_chart_tooltip(frame: &mut Frame<'_>, bounds: Rect, mouse: (u16, u16), text: &str) { @@ -2803,7 +2799,7 @@ fn render_quit_confirmation(frame: &mut Frame<'_>, area: Rect, state: &mut AppSt 1, ); let checkbox_text = if state.quit_dont_ask_again { - "[✓] Don't show again" + "[x] Don't show again" } else { "[ ] Don't show again" }; @@ -2922,7 +2918,7 @@ fn render_quit_preference_confirmation(frame: &mut Frame<'_>, area: Rect, state: } fn checkbox_span(checked: bool, label: Option<&str>) -> Span<'static> { - let mark = if checked { "✓" } else { " " }; + let mark = if checked { "x" } else { " " }; let text = match label { Some(label) => format!("[{mark}] {label}"), None => format!(" [{mark}] "), @@ -3805,7 +3801,7 @@ mod tests { fn quit_action_is_on_the_bottom_right_border() { let (title, targets) = quit_title(Rect::new(4, 2, 71, 20), true, true); - assert_eq!(title.spans[0].content.as_ref(), " [✓] "); + assert_eq!(title.spans[0].content.as_ref(), " [x] "); assert_eq!(title.spans[1].style.fg, Some(Color::Black)); assert_eq!(title.spans[1].style.bg, Some(Color::White)); assert_eq!(targets.len(), 2); @@ -4191,7 +4187,7 @@ mod tests { assert_eq!( format_vertical_bar_tooltip("2026-07-22", 45_456_785, UsageMetric::Tokens, formatter,), - "2026-07-22 · 45,456,785 tokens" + "2026-07-22 | 45,456,785 tokens" ); assert_eq!( chart_tooltip_rect(Rect::new(2, 2, 20, 10), (20, 3), 10), From 6cc5db3250dc8915ecea71fabad789317e46b9c4 Mon Sep 17 00:00:00 2001 From: woffko <2505149+woffko@users.noreply.github.com> Date: Thu, 23 Jul 2026 06:43:30 +0300 Subject: [PATCH 4/4] Fix fork replay accounting and date title --- src/ui/mod.rs | 12 +- src/usage/mod.rs | 289 +++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 276 insertions(+), 25 deletions(-) diff --git a/src/ui/mod.rs b/src/ui/mod.rs index ca3f33f..2bc30f9 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -3025,7 +3025,10 @@ fn card( } fn today_card_title(formatter: DisplayFormatter<'_>, now: NaiveDateTime) -> String { - format!("TODAY {}", formatter.format_session_datetime(now)) + format!( + "TODAY_{}", + formatter.format_session_datetime(now).replace(' ', "_") + ) } fn card4( @@ -3787,7 +3790,12 @@ mod tests { .and_hms_opt(14, 35, 0) .unwrap(); - assert_eq!(today_card_title(formatter, now), "TODAY 2026-07-22 14:35"); + assert_eq!(today_card_title(formatter, now), "TODAY_2026-07-22_14:35"); + + for style in [DisplayStyle::SystemCompact, DisplayStyle::SystemFull] { + let formatter = DisplayFormatter::new(style, &system_locale); + assert_eq!(today_card_title(formatter, now), "TODAY_07/22/2026_14:35"); + } } #[test] diff --git a/src/usage/mod.rs b/src/usage/mod.rs index 925ce22..6dc724c 100644 --- a/src/usage/mod.rs +++ b/src/usage/mod.rs @@ -18,7 +18,7 @@ const DEFAULT_MAX_SESSION_FILES_SCANNED: usize = 10_000; const DEFAULT_MAX_JSONL_LINE_BYTES: usize = 512 * 1024; const DEFAULT_SCAN_TIME_BUDGET_MS: u64 = 1500; const MAX_DISTINCT_MODELS: usize = 5_000; -const SCAN_CACHE_DB_SCHEMA_VERSION: i64 = 3; +const SCAN_CACHE_DB_SCHEMA_VERSION: i64 = 4; const FORK_REPLAY_END_GAP_MS: i64 = 1_000; const FORK_REPLAY_NO_TOKEN_GRACE_MS: i64 = 2_000; pub const DEFAULT_SCAN_CACHE_MAX_ENTRIES: usize = 50_000; @@ -1206,12 +1206,15 @@ fn parse_file_summary( session_cwd = extract_cwd(&value); } - if entry_type == "session_meta" { - maybe_start_fork_replay(&value, &mut first_session_meta_seen, &mut fork_replay); - } + let started_fork_replay = if entry_type == "session_meta" { + maybe_start_fork_replay(&value, &mut first_session_meta_seen, &mut fork_replay) + } else { + false + }; let event_timestamp_ms = read_timestamp_ms(&value); - let skip_fork_replay = fork_replay_should_skip_event(&mut fork_replay, event_timestamp_ms); + let skip_fork_replay = started_fork_replay + || fork_replay_should_skip_event(&mut fork_replay, event_timestamp_ms); if entry_type == "turn_context" { if !skip_fork_replay { @@ -1491,8 +1494,10 @@ fn open_or_init_scan_cache_db(path: &Path) -> Result { if schema_version == 1 || !has_v2_columns { migrate_scan_cache_db_v1_to_v2(&conn, path)?; } + if schema_version < 4 { + invalidate_forked_session_cache_rows(&conn, path)?; + } if schema_version < SCAN_CACHE_DB_SCHEMA_VERSION { - migrate_scan_cache_db_to_v3(&conn, path)?; conn.execute( "UPDATE cache_meta SET value = ?1 WHERE key = 'schema_version';", params![SCAN_CACHE_DB_SCHEMA_VERSION], @@ -1576,9 +1581,10 @@ fn migrate_scan_cache_db_v1_to_v2(conn: &Connection, path: &Path) -> Result<()> Ok(()) } -fn migrate_scan_cache_db_to_v3(conn: &Connection, path: &Path) -> Result<()> { - // v3 changes forked-session accounting semantics. Reusing those old rows would keep - // inflated totals, but non-forked cache rows are still valid. +fn invalidate_forked_session_cache_rows(conn: &Connection, path: &Path) -> Result<()> { + // v3 and v4 change forked-session accounting semantics. Reusing older fork rows + // would keep inflated totals, but non-forked cache rows are still valid. One pass + // is sufficient even when upgrading across multiple schema versions at once. let mut stmt = conn .prepare("SELECT file_path FROM file_cache;") .with_context(|| { @@ -1603,7 +1609,7 @@ fn migrate_scan_cache_db_to_v3(conn: &Connection, path: &Path) -> Result<()> { path.display() ) })?; - if cached_session_needs_v3_reparse(&file_path) { + if forked_session_cache_needs_reparse(&file_path) { stale_paths.push(file_path); } } @@ -1625,7 +1631,7 @@ fn migrate_scan_cache_db_to_v3(conn: &Connection, path: &Path) -> Result<()> { Ok(()) } -fn cached_session_needs_v3_reparse(file_path: &str) -> bool { +fn forked_session_cache_needs_reparse(file_path: &str) -> bool { let file = match File::open(file_path) { Ok(file) => file, Err(_) => return true, @@ -2180,28 +2186,29 @@ fn maybe_start_fork_replay( value: &Value, first_session_meta_seen: &mut bool, replay: &mut ForkReplayState, -) { +) -> bool { if *first_session_meta_seen { - return; + return false; } *first_session_meta_seen = true; let payload = value.get("payload").and_then(Value::as_object); let Some(payload) = payload else { - return; + return false; }; if payload .get("forked_from_id") .and_then(Value::as_str) .is_none() { - return; + return false; } - let start_ms = payload - .get("timestamp") - .and_then(parse_timestamp_value_ms) - .or_else(|| read_timestamp_ms(value)); + // The outer timestamp records when this JSONL event was emitted. The payload + // timestamp can be earlier because preparing a large fork replay takes time; + // using it as last_event_ms can falsely look like the end-of-replay gap. + let start_ms = read_timestamp_ms(value) + .or_else(|| payload.get("timestamp").and_then(parse_timestamp_value_ms)); *replay = ForkReplayState { active: true, done: false, @@ -2209,6 +2216,7 @@ fn maybe_start_fork_replay( last_event_ms: start_ms, token_events: 0, }; + true } fn fork_replay_should_skip_event( @@ -2224,21 +2232,24 @@ fn fork_replay_should_skip_event( }; let start_ms = replay.start_ms.unwrap_or(timestamp_ms); let elapsed_ms = timestamp_ms - start_ms; - let gap_ms = replay - .last_event_ms + let previous_event_ms = replay.last_event_ms; + let gap_ms = previous_event_ms .map(|last_ms| timestamp_ms - last_ms) .unwrap_or(0); + let monotonic_event_ms = previous_event_ms + .map(|last_ms| last_ms.max(timestamp_ms)) + .unwrap_or(timestamp_ms); if gap_ms >= FORK_REPLAY_END_GAP_MS || (replay.token_events == 0 && elapsed_ms >= FORK_REPLAY_NO_TOKEN_GRACE_MS) { replay.active = false; replay.done = true; - replay.last_event_ms = Some(timestamp_ms); + replay.last_event_ms = Some(monotonic_event_ms); return false; } - replay.last_event_ms = Some(timestamp_ms); + replay.last_event_ms = Some(monotonic_event_ms); true } @@ -2649,6 +2660,64 @@ mod tests { append_agent_message_line(path, timestamp_ms + 1_800); } + fn write_delayed_fork_replay_prefix( + path: &Path, + payload_timestamp_ms: i64, + outer_delay_ms: i64, + ) -> i64 { + let payload_timestamp = Utc + .timestamp_millis_opt(payload_timestamp_ms) + .single() + .expect("valid payload timestamp") + .to_rfc3339(); + let outer_timestamp_ms = payload_timestamp_ms + outer_delay_ms; + let outer_timestamp = Utc + .timestamp_millis_opt(outer_timestamp_ms) + .single() + .expect("valid outer timestamp") + .to_rfc3339(); + append_json_line( + path, + serde_json::json!({ + "type": "session_meta", + "timestamp": outer_timestamp, + "payload": { + "id": "fork-child", + "forked_from_id": "parent", + "timestamp": payload_timestamp, + "cwd": "/tmp/forked-project" + } + }), + ); + append_json_line( + path, + serde_json::json!({ + "type": "session_meta", + "timestamp": Utc + .timestamp_millis_opt(outer_timestamp_ms + 1) + .single() + .expect("valid replay metadata timestamp") + .to_rfc3339(), + "payload": { + "id": "parent", + "timestamp": payload_timestamp, + "cwd": "/tmp/forked-project" + } + }), + ); + append_total_token_line(path, outer_timestamp_ms + 2, 1_000, 800, 100); + for offset in 3..67 { + append_agent_message_line(path, outer_timestamp_ms + offset); + } + append_total_token_line(path, outer_timestamp_ms + 100, 1_500, 1_300, 130); + outer_timestamp_ms + } + + fn append_fork_replay_live_tail(path: &Path, outer_timestamp_ms: i64) { + append_total_token_line(path, outer_timestamp_ms + 4_000, 1_700, 1_400, 150); + append_agent_message_line(path, outer_timestamp_ms + 4_001); + } + fn default_test_limits(full_scan: bool) -> ScanLimits { ScanLimits { max_session_file_bytes: 4 * 1024 * 1024, @@ -2687,6 +2756,24 @@ mod tests { assert_eq!(monday.weekday(), Weekday::Mon); } + #[test] + fn fork_replay_keeps_last_event_timestamp_monotonic() { + let mut replay = ForkReplayState { + active: true, + done: false, + start_ms: Some(1_000), + last_event_ms: Some(1_000), + token_events: 1, + }; + + assert!(fork_replay_should_skip_event(&mut replay, Some(1_100))); + assert!(fork_replay_should_skip_event(&mut replay, Some(900))); + assert_eq!(replay.last_event_ms, Some(1_100)); + assert!(fork_replay_should_skip_event(&mut replay, Some(1_150))); + assert!(replay.active); + assert!(!replay.done); + } + #[test] fn compute_snapshot_ignores_forked_session_replay_without_cache() { let root = make_temp_dir("fork-replay-no-cache"); @@ -2754,6 +2841,93 @@ mod tests { let _ = std::fs::remove_dir_all(root); } + #[test] + fn compute_snapshot_ignores_delayed_fork_metadata_replay() { + let root = make_temp_dir("fork-replay-delayed-metadata"); + let codex_home = root.join("codex"); + let sessions_root = codex_home.join("sessions"); + std::fs::create_dir_all(&sessions_root).expect("create sessions root"); + + let payload_timestamp_ms = + Utc::now().timestamp_millis() - Duration::hours(1).num_milliseconds(); + let session_path = sessions_root.join("forked.jsonl"); + let outer_timestamp_ms = + write_delayed_fork_replay_prefix(&session_path, payload_timestamp_ms, 1_500); + append_fork_replay_live_tail(&session_path, outer_timestamp_ms); + + let snapshot = compute_snapshot(30, &codex_home, None, default_test_limits(false), None) + .expect("snapshot"); + assert_eq!(snapshot.totals.last30_days_tokens, 220); + assert_eq!( + snapshot.days.iter().map(|day| day.agent_runs).sum::(), + 1, + "the delayed outer session timestamp must not expose compressed replay runs" + ); + + let _ = std::fs::remove_dir_all(root); + } + + #[test] + fn compute_snapshot_resumes_active_delayed_fork_replay_from_cache() { + let root = make_temp_dir("fork-replay-delayed-resume"); + let codex_home = root.join("codex"); + let sessions_root = codex_home.join("sessions"); + std::fs::create_dir_all(&sessions_root).expect("create sessions root"); + + let payload_timestamp_ms = + Utc::now().timestamp_millis() - Duration::hours(1).num_milliseconds(); + let session_path = sessions_root.join("forked.jsonl"); + let outer_timestamp_ms = + write_delayed_fork_replay_prefix(&session_path, payload_timestamp_ms, 1_500); + let cache_db_path = root.join("comon.db"); + + let replay_only = compute_snapshot( + 30, + &codex_home, + None, + default_test_limits(false), + Some(cache_db_path.as_path()), + ) + .expect("replay-only snapshot"); + assert_eq!(replay_only.totals.last30_days_tokens, 0); + assert_eq!( + replay_only + .days + .iter() + .map(|day| day.agent_runs) + .sum::(), + 0 + ); + + append_fork_replay_live_tail(&session_path, outer_timestamp_ms); + let resumed = compute_snapshot( + 30, + &codex_home, + None, + default_test_limits(false), + Some(cache_db_path.as_path()), + ) + .expect("resumed snapshot"); + assert_eq!(resumed.totals.last30_days_tokens, 220); + assert_eq!( + resumed.days.iter().map(|day| day.agent_runs).sum::(), + 1 + ); + + let cached = compute_snapshot( + 30, + &codex_home, + None, + default_test_limits(false), + Some(cache_db_path.as_path()), + ) + .expect("cached resumed snapshot"); + assert_eq!(cached.totals.last30_days_tokens, 220); + assert_eq!(cached.days.iter().map(|day| day.agent_runs).sum::(), 1); + + let _ = std::fs::remove_dir_all(root); + } + #[test] fn compute_snapshot_builds_project_activity_sorted_by_last_activity() { let root = make_temp_dir("project-activity"); @@ -3248,6 +3422,75 @@ mod tests { let _ = std::fs::remove_dir_all(root); } + #[test] + fn open_or_init_scan_cache_db_v4_reparses_only_forked_rows() { + let root = make_temp_dir("cache-migrate-v4"); + let sessions_root = root.join("sessions"); + std::fs::create_dir_all(&sessions_root).expect("create sessions root"); + let keep_path = sessions_root.join("keep.jsonl"); + let forked_path = sessions_root.join("forked.jsonl"); + let now_ms = Utc::now().timestamp_millis(); + write_token_file(&keep_path, now_ms, 10, 5); + write_delayed_fork_replay_prefix(&forked_path, now_ms, 1_500); + + let db_path = root.join("comon.db"); + let conn = Connection::open(&db_path).expect("open v3 db"); + conn.execute_batch( + " + CREATE TABLE cache_meta ( + key TEXT PRIMARY KEY, + value INTEGER NOT NULL + ); + INSERT INTO cache_meta(key, value) VALUES('schema_version', 3); + CREATE TABLE file_cache ( + file_path TEXT PRIMARY KEY, + file_size INTEGER NOT NULL, + file_mtime INTEGER, + file_offset INTEGER NOT NULL DEFAULT 0, + fully_parsed INTEGER NOT NULL DEFAULT 1, + session_cwd TEXT, + parser_state_json TEXT NOT NULL DEFAULT '{}', + daily_json TEXT NOT NULL, + model_daily_json TEXT NOT NULL, + updated_at INTEGER NOT NULL + ); + ", + ) + .expect("create v3 schema"); + for path in [&keep_path, &forked_path] { + conn.execute( + " + INSERT INTO file_cache( + file_path, file_size, file_mtime, file_offset, fully_parsed, + session_cwd, parser_state_json, daily_json, model_daily_json, updated_at + ) VALUES(?1, 1, 1, 1, 1, '/tmp', '{}', '{}', '{}', 1); + ", + rusqlite::params![path.to_string_lossy().to_string()], + ) + .expect("insert cache row"); + } + drop(conn); + + let db = open_or_init_scan_cache_db(&db_path).expect("open and migrate"); + let (store, _) = load_scan_cache_store(&db).expect("load migrated cache"); + let keep_key = keep_path.to_string_lossy().to_string(); + let forked_key = forked_path.to_string_lossy().to_string(); + assert!(store.entries.contains_key(&keep_key)); + assert!(!store.entries.contains_key(&forked_key)); + + let schema_version: i64 = db + .conn + .query_row( + "SELECT value FROM cache_meta WHERE key = 'schema_version';", + [], + |row| row.get(0), + ) + .expect("read schema version"); + assert_eq!(schema_version, 4); + + let _ = std::fs::remove_dir_all(root); + } + #[test] fn open_or_init_scan_cache_db_migrates_v1_schema_and_clears_stale_rows() { let root = make_temp_dir("cache-migrate");