Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 27 additions & 14 deletions apps/desktop-tauri/src-tauri/src/commands/bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,22 @@ impl RateWindowSnapshot {
}
}

/// The core window this snapshot was built from, as far as pace needs it:
/// usage, length, reset time and reset description.
pub(crate) fn to_rate_window(&self) -> RateWindow {
let resets_at = self
.resets_at
.as_deref()
.and_then(|value| chrono::DateTime::parse_from_rfc3339(value).ok())
.map(|value| value.with_timezone(&chrono::Utc));
RateWindow::with_details(
self.used_percent,
self.window_minutes,
resets_at,
self.reset_description.clone(),
)
}

/// Enrich with raw reserve info derived from pace analysis.
/// delta_percent = actual - expected; negative means ahead (in reserve).
/// Only meaningful for longer windows (weekly); skip if reserve rounds to 0.
Expand Down Expand Up @@ -208,18 +224,6 @@ pub struct PaceSnapshot {
/// Block of the window this pace comes from (upstream hides its pace).
#[serde(default)]
pub monthly_limit_block: Option<MonthlyLimitBlockSnapshot>,
/// Whether the window this pace comes from carries its own length.
/// Upstream `UsageStore.weeklyPace` (UsageStore+HistoricalPace.swift)
/// gives the menu bar no pace for a generic window without
/// `windowMinutes`, rather than scoring it against the 7-day fallback,
/// so the tray pace tint skips it. Card lanes keep their own pace.
/// Tray-only; not sent to the frontend.
#[serde(default = "default_window_has_length", skip_serializing)]
pub window_has_length: bool,
}

fn default_window_has_length() -> bool {
true
}

/// One burndown chart point (RFC 3339 capture time + remaining percent).
Expand Down Expand Up @@ -353,6 +357,11 @@ pub struct ProviderUsageSnapshot {
pub error_state: codexbar::core::ProviderStateKind,
#[serde(default)]
pub pace: Option<PaceSnapshot>,
/// Whether the fetch allows pace (upstream `allowsPace(dataConfidence:)`).
/// The tray scores its pace tint at presentation time, so it needs this
/// beside the windows. Tray-only; not sent to the frontend.
#[serde(default = "default_pace_authoritative", skip_serializing)]
pub pace_authoritative: bool,
#[serde(default)]
pub account_organization: Option<String>,
#[serde(default)]
Expand Down Expand Up @@ -380,6 +389,10 @@ fn default_source_label() -> String {
"seed".to_string()
}

fn default_pace_authoritative() -> bool {
true
}

fn default_error_state() -> codexbar::core::ProviderStateKind {
codexbar::core::ProviderStateKind::Unknown
}
Expand Down Expand Up @@ -475,8 +488,6 @@ impl ProviderUsageSnapshot {
expected_used_percent: p.expected_used_percent,
actual_used_percent: p.actual_used_percent,
monthly_limit_block: MonthlyLimitBlockSnapshot::for_pace(usage, &blocked),
window_has_length: primary_pace_window
.is_some_and(|window| window.window_minutes.is_some()),
});

// Pace of the secondary lane, which also derives the row's reserve.
Expand Down Expand Up @@ -685,6 +696,7 @@ impl ProviderUsageSnapshot {
error: None,
error_state: codexbar::core::ProviderStateKind::Ready,
pace,
pace_authoritative: allows_pace,
account_organization: usage.account_organization.clone(),
tray_status_label: None,
fetch_duration_ms: None,
Expand Down Expand Up @@ -744,6 +756,7 @@ impl ProviderUsageSnapshot {
error: Some(error),
error_state: state_kind,
pace: None,
pace_authoritative: true,
account_organization: None,
tray_status_label: None,
fetch_duration_ms: None,
Expand Down
48 changes: 19 additions & 29 deletions apps/desktop-tauri/src-tauri/src/commands/bridge/pace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,15 @@ mod tests {

assert_eq!(snap.primary.pace, None);
assert_eq!(snap.secondary.as_ref().unwrap().pace, None);
// The tray scores its own pace, so it needs the flag too.
assert!(!snap.pace_authoritative);
assert!(
snapshot(
UsageSnapshot::new(resetting_in(now, 60.0, Some(300), 150)),
true
)
.pace_authoritative
);
}

#[test]
Expand Down Expand Up @@ -386,18 +395,17 @@ mod tests {
// Extras keep plain lane pace, as on the Mac: a 14-day reset does not
// fit the 7-day fallback, so the seat-credit window shows none.
assert_eq!(snap.extra_rate_windows[0].window.pace, None);
// The provider-level pace (tray colour) needs a window length, as
// upstream `UsageStore.weeklyPace` does, so it stays unset here.
// The provider-level pace (the Settings detail) scores the length-less
// window against the 7-day fallback, which a 14-day reset overruns.
assert!(snap.pace.is_none());
}

/// Within the 7-day fallback, a length-less Copilot window still yields
/// a provider-level pace (the Settings detail shows it), but upstream
/// `UsageStore.weeklyPace` gives the menu bar none for a window without
/// `windowMinutes`, so it is marked as having no length for the tray.
/// The card lane keeps its reset-window pace.
/// a provider-level pace (the Settings detail shows it). The card lane
/// keeps its reset-window pace. The tray scores its own pace and skips
/// the window (`tray_presentation::menu_bar_pace`).
#[test]
fn copilot_provider_pace_is_marked_lengthless_for_the_tray() {
fn copilot_lengthless_window_keeps_provider_and_lane_pace() {
let now = february_now();
let usage = UsageSnapshot::new(RateWindow::with_details(
60.0,
Expand All @@ -408,29 +416,11 @@ mod tests {

let snap = snapshot_at(ProviderId::Copilot, usage);

let pace = snap.pace.as_ref().expect("provider-level pace");
assert!(!pace.window_has_length);
assert!(snap.pace.is_some());
assert!(lane_pace(&snap.primary).reset_window);
let json = serde_json::to_value(pace).unwrap();
assert!(json.get("windowHasLength").is_none());
}

#[test]
fn provider_pace_from_a_window_with_a_length_keeps_the_tray_tint() {
let now = february_now();
let usage = UsageSnapshot::new(RateWindow::with_details(
60.0,
Some(10_080),
Some(now + Duration::days(3)),
None,
));

let snap = snapshot_at(ProviderId::Claude, usage);

assert!(snap.pace.as_ref().expect("pace").window_has_length);
let cached: PaceSnapshot =
serde_json::from_str(r#"{"stage":"behind","deltaPercent":-8.0}"#).unwrap();
assert!(cached.window_has_length);
assert!(snap.pace_authoritative);
let json = serde_json::to_value(&snap).unwrap();
assert!(json.get("paceAuthoritative").is_none());
}

/// Cursor's plan bars carry the billing-cycle length; the API bar is the
Expand Down
20 changes: 4 additions & 16 deletions apps/desktop-tauri/src-tauri/src/commands/chart.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
use crate::commands::bridge::RateWindowSnapshot;
use crate::state::AppState;
use chrono::{DateTime, Utc};
use codexbar::core::{OpenAIDashboardCacheStore, RateWindow};
use codexbar::core::OpenAIDashboardCacheStore;
use codexbar::cost_reporting_period::{CostReportingPeriod, CostTimeZone};
use codexbar::cost_scanner::{
CostScanner, CostSummary, TodayUsage, get_daily_cost_history, get_daily_token_history,
Expand Down Expand Up @@ -221,7 +221,9 @@ fn build_provider_chart_data_with_cancel(
..ProviderChartData::empty(provider_id)
};
}
let live_window = weekly_window.as_ref().map(rate_window_from_snapshot);
let live_window = weekly_window
.as_ref()
.map(RateWindowSnapshot::to_rate_window);
let build_snapshot = || {
codexbar::providers::chart::build_chart_snapshot(
&provider_id,
Expand Down Expand Up @@ -466,20 +468,6 @@ fn local_usage_summary_from_cost_summary(
})
}

fn rate_window_from_snapshot(snapshot: &RateWindowSnapshot) -> RateWindow {
let resets_at = snapshot
.resets_at
.as_deref()
.and_then(|value| DateTime::parse_from_rfc3339(value).ok())
.map(|value| value.with_timezone(&Utc));
RateWindow::with_details(
snapshot.used_percent,
snapshot.window_minutes,
resets_at,
snapshot.reset_description.clone(),
)
}

fn active_chart_scans() -> &'static Mutex<HashMap<String, Arc<AtomicBool>>> {
static ACTIVE: OnceLock<Mutex<HashMap<String, Arc<AtomicBool>>>> = OnceLock::new();
ACTIVE.get_or_init(|| Mutex::new(HashMap::new()))
Expand Down
1 change: 1 addition & 0 deletions apps/desktop-tauri/src-tauri/src/commands/providers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1722,6 +1722,7 @@ mod reset_backfill_tests {
error: None,
error_state: codexbar::core::ProviderStateKind::Ready,
pace: None,
pace_authoritative: true,
account_organization: None,
tray_status_label: None,
fetch_duration_ms: None,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ mod tests {
error: None,
error_state: codexbar::core::ProviderStateKind::Ready,
pace: None,
pace_authoritative: true,
account_organization: None,
tray_status_label: None,
fetch_duration_ms: None,
Expand Down
2 changes: 2 additions & 0 deletions apps/desktop-tauri/src-tauri/src/powertoys.rs
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ mod tests {
error: None,
error_state: codexbar::core::ProviderStateKind::Ready,
pace: None,
pace_authoritative: true,
account_organization: Some("Example Org".to_string()),
tray_status_label: None,
fetch_duration_ms: None,
Expand Down Expand Up @@ -276,6 +277,7 @@ mod tests {
error: None,
error_state: codexbar::core::ProviderStateKind::Ready,
pace: None,
pace_authoritative: true,
account_organization: None,
tray_status_label: None,
fetch_duration_ms: None,
Expand Down
1 change: 1 addition & 0 deletions apps/desktop-tauri/src-tauri/src/tray_bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -778,6 +778,7 @@ mod tests {
error: None,
error_state: codexbar::core::ProviderStateKind::Ready,
pace: None,
pace_authoritative: true,
account_organization: None,
tray_status_label: None,
fetch_duration_ms: None,
Expand Down
81 changes: 73 additions & 8 deletions apps/desktop-tauri/src-tauri/src/tray_presentation.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
//! Pure tray presentation policy shared by the native tray surfaces.

use crate::commands::{ProviderUsageSnapshot, RateWindowSnapshot};
use chrono::{DateTime, Utc};
use codexbar::core::{IconLane, ProviderId, UsagePace};
use codexbar::settings::{Language, MetricPreference, Settings, TrayIconMode};
use codexbar::tray::{
TrayPaceColor, render_bar_icon_rgba_with_pace, render_percent_icon_rgba_with_pace,
Expand Down Expand Up @@ -56,6 +58,16 @@ impl<'a> TrayPresentationPlan<'a> {
settings: &'a Settings,
snapshots: &'a [ProviderUsageSnapshot],
rates_cache: Option<&'a crate::commands::CurrencyRateCache>,
) -> Self {
Self::resolve_at(settings, snapshots, rates_cache, Utc::now())
}

/// [`Self::resolve`] with the pace tint scored at `now`.
pub(crate) fn resolve_at(
settings: &'a Settings,
snapshots: &'a [ProviderUsageSnapshot],
rates_cache: Option<&'a crate::commands::CurrencyRateCache>,
now: DateTime<Utc>,
) -> Self {
let ordered = ordered_snapshot_refs(settings, snapshots);
let healthy = ordered
Expand All @@ -66,7 +78,7 @@ impl<'a> TrayPresentationPlan<'a> {
// The retired Display mode "minimal" is folded into this flag once on load.
let prefer_highest = settings.menu_bar_shows_highest_usage;
let selected = pick_tray_provider(&healthy, prefer_highest);
let icon_pace = tray_pace_color(settings, selected);
let icon_pace = tray_pace_color(settings, selected, now);

let (icon, status_rows) = match settings.tray_icon_mode {
TrayIconMode::Stacked => {
Expand Down Expand Up @@ -219,20 +231,73 @@ fn resolve_single_provider_icon_plan(
/// Pace tint for the provider that drives a single-provider tray icon.
///
/// Opt-in through `menu_bar_color_pace`; on-track and unknown stages keep the
/// normal usage-level colour. A pace scored from a window without its own
/// length gets no tint, as upstream `UsageStore.weeklyPace` gives the menu
/// bar no pace for it (Copilot's reset-date-only windows, for example).
/// normal usage-level colour. The pace comes from the menu bar pace window
/// (upstream `menuBarPaceWindow`, StatusItemController+Animation.swift
/// :1207-1225): the lane the provider declares, or else the window the icon
/// draws for the selected metric.
fn tray_pace_color(
settings: &Settings,
selected: Option<&ProviderUsageSnapshot>,
now: DateTime<Utc>,
) -> Option<TrayPaceColor> {
if !settings.menu_bar_color_pace {
return None;
}
selected
.and_then(|snapshot| snapshot.pace.as_ref())
.filter(|pace| pace.window_has_length)
.and_then(|pace| TrayPaceColor::from_stage(&pace.stage))
let snapshot = selected?;
let window = menu_bar_pace_window(snapshot, settings)?;
let pace = menu_bar_pace(snapshot, &window, settings.weekly_progress_work_days, now)?;
TrayPaceColor::from_stage(crate::commands::pace::stage_str(pace.stage))
}

/// Window the menu bar pace is scored on. A provider-declared lane wins over
/// the selected metric; a missing declared lane gives no pace.
fn menu_bar_pace_window(
snapshot: &ProviderUsageSnapshot,
settings: &Settings,
) -> Option<RateWindowSnapshot> {
let declared = ProviderId::from_cli_name(&snapshot.provider_id)
.and_then(|id| codexbar::core::instantiate_provider(id).menu_bar_pace_lane());
match declared {
Some(IconLane::Primary) => Some(snapshot.primary.clone()),
Some(IconLane::Secondary) => snapshot.secondary.clone(),
None => Some(crate::usage_metric::selected_usage_icon_windows(snapshot, settings).0),
}
}

/// Generic branch of upstream `UsageStore.weeklyPace`
/// (UsageStore+HistoricalPace.swift:8-64): the fetch must allow pace and the
/// window must have quota left and its own length (no 7-day fallback). A
/// monthly sentinel resolves to the real calendar cycle first
/// (`paceWindowForElapsedEligibility`, :118-124), and a pace expecting less
/// than 3% use is dropped.
fn menu_bar_pace(
snapshot: &ProviderUsageSnapshot,
window: &RateWindowSnapshot,
work_days: Option<u8>,
now: DateTime<Utc>,
) -> Option<UsagePace> {
const MINIMUM_EXPECTED_PERCENT: f64 = 3.0;
if !snapshot.pace_authoritative || window.is_informational {
return None;
}
window.window_minutes?;
let rate_window = window.to_rate_window();
if rate_window.remaining_percent() <= 0.0 {
return None;
}
let pace_window = match ProviderId::from_cli_name(&snapshot.provider_id) {
Some(id) => id
.pace_capability()
.resolved_reset_window_for_pace(&rate_window),
None => rate_window,
};
let pace = UsagePace::weekly(
&pace_window,
Some(now),
crate::commands::pace::WEEKLY_FALLBACK_MINUTES,
work_days,
)?;
(pace.expected_used_percent >= MINIMUM_EXPECTED_PERCENT).then_some(pace)
}

fn fallback_percents(
Expand Down
Loading