From c66156b35baa57ad718b693a5a0d7ff36b06d2b0 Mon Sep 17 00:00:00 2001 From: tsouth89 Date: Tue, 4 Aug 2026 21:06:55 -0400 Subject: [PATCH] Fix release profile, Devin percent scale, and window alert coverage These changes were already in the working tree; they are split out here so the pace and Cursor work lands on a reviewable base. - Move the release profile to the workspace root. Cargo only reads profiles from there, so `rust/Cargo.toml` held a block cargo ignored and warned about on every build: shipped binaries had no LTO, no symbol stripping, and 16 codegen units. `panic = "abort"` is deliberately not carried over, since it has never applied to a shipped build and would turn a panic in a background refresh into an immediate process kill. - Resolve Devin's percent scale across all reported windows at once. A single value cannot say whether `0.23` means 23% or 0.23%, so a response holding `0.4` beside `32` now reads the `0.4` as 0.4% instead of rescaling it to 40%. - Let a model pool raise usage alerts alongside the session and weekly windows. Claude's Opus allowance could sit at 99% in silence. Monthly windows stay out on purpose: crossing a threshold mid-cycle is normal there, not news. - Key threshold alerts by cadence rather than slot, so a promoted weekly window is never reported as a session. - Taskbar flyout: give the strip marker the right edge so chips line up across rows. --- Cargo.toml | 15 ++ .../src-tauri/src/capacity_events.rs | 21 ++ .../src-tauri/src/commands/providers.rs | 137 ++++++++++--- .../src-tauri/src/quota_run_history.rs | 63 +++++- .../src-tauri/src/usage_history.rs | 182 +++++++++++++++++- .../src/lib/capacityPresentation.test.ts | 24 +++ .../src/lib/capacityPresentation.ts | 43 +++-- apps/desktop-tauri/src/styles.css | 24 ++- .../src/surfaces/TaskbarFlyout.tsx | 10 +- rust/Cargo.toml | 7 - rust/src/notifications.rs | 25 ++- rust/src/providers/devin/mod.rs | 71 ++++++- rust/src/providers/opencodego/mod.rs | 2 +- 13 files changed, 551 insertions(+), 73 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index bdface4f..bd35e3cd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,3 +5,18 @@ members = [ ] default-members = ["apps/desktop-tauri/src-tauri"] resolver = "3" + +# Cargo only reads profiles from the workspace root. These lived in +# `rust/Cargo.toml`, where cargo ignored them and said so on every build, so +# released binaries were built unoptimized-for-size: no LTO, no symbol +# stripping, and 16 codegen units. +# +# `panic = "abort"` was in that ignored block and is deliberately not carried +# over. It has never actually applied to a shipped build, and turning it on +# would convert a panic in any background refresh task into an immediate +# process kill — the failure 1.5.21 moved away from. +[profile.release] +opt-level = 3 +lto = true +codegen-units = 1 +strip = true diff --git a/apps/desktop-tauri/src-tauri/src/capacity_events.rs b/apps/desktop-tauri/src-tauri/src/capacity_events.rs index 1ae19eee..dcfc8775 100644 --- a/apps/desktop-tauri/src-tauri/src/capacity_events.rs +++ b/apps/desktop-tauri/src-tauri/src/capacity_events.rs @@ -751,6 +751,27 @@ fn observed_windows( window, ); } + // The model lane shares a cadence with the weekly window (Claude's 7-day + // Opus pool is also 10080 minutes), so a cadence-derived id would land on + // "weekly" and one window would silently replace the other in this map. + if let Some(window) = snapshot.model_specific.as_ref() + && let Some(mut observed) = to_observed_window("Model", window) + { + observed.id = "model".to_string(); + windows.insert(observed.id.clone(), observed); + } + // Same hazard for the third window: most are monthly, but a provider whose + // tertiary matches its secondary cadence must not overwrite it. Core slots + // are already in the map, so an id they hold means this one needs its own. + if let Some(window) = snapshot.tertiary.as_ref() { + let label = snapshot.tertiary_label.as_deref().unwrap_or("Extra"); + if let Some(mut observed) = to_observed_window(label, window) { + if windows.contains_key(&observed.id) { + observed.id = "tertiary".to_string(); + } + windows.insert(observed.id.clone(), observed); + } + } for extra in &snapshot.extra_rate_windows { if ignored_capacity_window(snapshot, &extra.id, &extra.title) { continue; diff --git a/apps/desktop-tauri/src-tauri/src/commands/providers.rs b/apps/desktop-tauri/src-tauri/src/commands/providers.rs index 364f1238..29ef3687 100644 --- a/apps/desktop-tauri/src-tauri/src/commands/providers.rs +++ b/apps/desktop-tauri/src-tauri/src/commands/providers.rs @@ -833,27 +833,75 @@ fn window_notify_key( } } -/// Plan per-window threshold alerts for a snapshot's primary/secondary windows. +/// Plan per-window threshold alerts for every measured window on a snapshot. /// /// Returns stable, cadence-based window keys (so a primary/secondary swap cannot /// masquerade as a new crossing) and the used-percent of the true 5-hour session /// window *only when it is actually present this refresh* — a promoted weekly is /// never reported as the session. +/// +/// The third and model windows are real subscription ceilings (OpenCode Go +/// Monthly, Claude's Opus pool), so they are offered to the notifier too — which +/// keeps its own policy about what deserves a toast and still declines monthly +/// keys. The model window carries its own key rather than a cadence one: +/// Claude's is 7 days like the weekly window, and a shared key would let +/// whichever came second be dropped as a duplicate. +/// The measured windows of one snapshot, borrowed for alert planning. Grouped +/// so adding a slot does not grow every signature along the way. +struct ThresholdWindows<'a> { + primary_label: Option<&'a str>, + primary: &'a RateWindowSnapshot, + secondary_label: Option<&'a str>, + secondary: Option<&'a RateWindowSnapshot>, + tertiary_label: Option<&'a str>, + tertiary: Option<&'a RateWindowSnapshot>, + model_specific: Option<&'a RateWindowSnapshot>, +} + +impl<'a> ThresholdWindows<'a> { + fn of(snapshot: &'a ProviderUsageSnapshot) -> Self { + Self { + primary_label: snapshot.primary_label.as_deref(), + primary: &snapshot.primary, + secondary_label: snapshot.secondary_label.as_deref(), + secondary: snapshot.secondary.as_ref(), + tertiary_label: snapshot.tertiary_label.as_deref(), + tertiary: snapshot.tertiary.as_ref(), + model_specific: snapshot.model_specific.as_ref(), + } + } +} + fn plan_threshold_alerts( provider: ProviderId, - primary_label: Option<&str>, - primary: &RateWindowSnapshot, - secondary_label: Option<&str>, - secondary: Option<&RateWindowSnapshot>, + windows: ThresholdWindows<'_>, ) -> (Vec<(&'static str, f64)>, Option) { let mut alerts: Vec<(&'static str, f64)> = Vec::new(); let mut session_percent: Option = None; let mut seen: std::collections::HashSet<&'static str> = std::collections::HashSet::new(); - for (label, window) in std::iter::once((primary_label, primary)) - .chain(secondary.map(|window| (secondary_label, window))) + for (key, window) in std::iter::once(( + window_notify_key( + provider, + windows.primary_label, + windows.primary.window_minutes, + ), + windows.primary, + )) + .chain(windows.secondary.map(|window| { + ( + window_notify_key(provider, windows.secondary_label, window.window_minutes), + window, + ) + })) + .chain(windows.tertiary.map(|window| { + ( + window_notify_key(provider, windows.tertiary_label, window.window_minutes), + window, + ) + })) + .chain(windows.model_specific.map(|window| ("model", window))) { - let key = window_notify_key(provider, label, window.window_minutes); if key == "session" { session_percent = Some(window.used_percent); } @@ -877,13 +925,8 @@ fn notify_usage_thresholds( if snapshot.error.is_none() && let Some(&provider) = cli_map.get(snapshot.provider_id.as_str()) { - let (alerts, _session_percent) = plan_threshold_alerts( - provider, - snapshot.primary_label.as_deref(), - &snapshot.primary, - snapshot.secondary_label.as_deref(), - snapshot.secondary.as_ref(), - ); + let (alerts, _session_percent) = + plan_threshold_alerts(provider, ThresholdWindows::of(snapshot)); for (window_key, used_percent) in alerts { guard.notification_manager.check_and_notify( provider, @@ -1122,10 +1165,15 @@ mod predictive_warning_tests { // 5-hour present: primary = 5h (100%), secondary = weekly (75%). let (alerts, session) = plan_threshold_alerts( ProviderId::Codex, - Some("Session"), - &rw(Some(300), 100.0), - Some("Weekly"), - Some(&rw(Some(10_080), 75.0)), + ThresholdWindows { + primary_label: Some("Session"), + primary: &rw(Some(300), 100.0), + secondary_label: Some("Weekly"), + secondary: Some(&rw(Some(10_080), 75.0)), + tertiary_label: None, + tertiary: None, + model_specific: None, + }, ); assert_eq!(session, Some(100.0)); assert!(alerts.contains(&("session", 100.0))); @@ -1135,10 +1183,15 @@ mod predictive_warning_tests { // It must NOT be reported as the session, and it keeps the "weekly" key. let (alerts, session) = plan_threshold_alerts( ProviderId::Codex, - Some("Weekly"), - &rw(Some(10_080), 75.0), - None, - None, + ThresholdWindows { + primary_label: Some("Weekly"), + primary: &rw(Some(10_080), 75.0), + secondary_label: None, + secondary: None, + tertiary_label: None, + tertiary: None, + model_specific: None, + }, ); assert_eq!( session, None, @@ -1147,6 +1200,44 @@ mod predictive_warning_tests { assert_eq!(alerts, vec![("weekly", 75.0)]); } + /// The third and model windows are real ceilings. They were never handed to + /// the notifier, so an OpenCode Go monthly pool or a Claude Opus pool could + /// sit at 99% without a word. + #[test] + fn third_and_model_windows_alert_too() { + let (alerts, _) = plan_threshold_alerts( + ProviderId::OpenCodeGo, + ThresholdWindows { + primary_label: Some("Rolling (5h)"), + primary: &rw(Some(300), 34.0), + secondary_label: Some("Weekly"), + secondary: Some(&rw(Some(10_080), 32.0)), + tertiary_label: Some("Monthly"), + tertiary: Some(&rw(Some(43_200), 97.0)), + model_specific: None, + }, + ); + assert!(alerts.contains(&("monthly", 97.0))); + + // Claude's model pool runs on the same 7-day cadence as its weekly + // window, so a cadence key would collide and the second one would be + // dropped as a duplicate. The model window carries its own key. + let (alerts, _) = plan_threshold_alerts( + ProviderId::Claude, + ThresholdWindows { + primary_label: Some("Session (5h)"), + primary: &rw(Some(300), 4.0), + secondary_label: Some("Weekly"), + secondary: Some(&rw(Some(10_080), 20.0)), + tertiary_label: None, + tertiary: None, + model_specific: Some(&rw(Some(10_080), 96.0)), + }, + ); + assert!(alerts.contains(&("weekly", 20.0))); + assert!(alerts.contains(&("model", 96.0))); + } + /// A payload for eligibility checks only; the numbers do not matter. #[cfg(test)] fn eligibility_event( diff --git a/apps/desktop-tauri/src-tauri/src/quota_run_history.rs b/apps/desktop-tauri/src-tauri/src/quota_run_history.rs index cc78d8c0..23cdb6ad 100644 --- a/apps/desktop-tauri/src-tauri/src/quota_run_history.rs +++ b/apps/desktop-tauri/src-tauri/src/quota_run_history.rs @@ -666,7 +666,15 @@ fn live_windows(snapshot: &ProviderUsageSnapshot) -> Vec { push_live(&mut windows, snapshot, "model", Some("Model"), window); } if let Some(window) = snapshot.tertiary.as_ref() { - push_live(&mut windows, snapshot, "tertiary", Some("API"), window); + // Same fallback as usage history: use the window's own label when it has + // one, so an OpenCode Go monthly run is not filed as an API run. + push_live( + &mut windows, + snapshot, + "tertiary", + snapshot.tertiary_label.as_deref().or(Some("API")), + window, + ); } for extra in &snapshot.extra_rate_windows { push_live( @@ -699,6 +707,16 @@ fn push_live( if id.is_empty() { return; } + // Cadence alone does not separate every slot: Claude's model pool is 7 days + // like its weekly window, so both resolved to "weekly" and the model reading + // took over the weekly run — same open key, wrong numbers and label. Slots + // are pushed core-first, so an id already taken belongs to the core window + // and this one falls back to its own slot name. + let id = if windows.iter().any(|existing| existing.id == id) { + fallback_id.to_string() + } else { + id + }; windows.push(LiveWindow { id, label: label.to_string(), @@ -1041,6 +1059,49 @@ mod tests { assert!(list_runs(provider, Some("me@job.test"), Some("acct-personal")).is_empty()); } + #[test] + fn a_model_window_does_not_take_over_the_weekly_run() { + // Claude's model pool is 7 days, the same cadence as its weekly window, + // so both resolved to the "weekly" id. Sharing one open key meant the + // model reading overwrote the weekly run's numbers and its label. + let now = Utc::now(); + let reset = now + Duration::hours(6); + let mut snap = snapshot("claude", "person@example.com", now, 10.0, reset); + snap.secondary = Some(rate(20.0, 10_080, reset)); + snap.secondary_label = Some("Weekly".into()); + snap.model_specific = Some(rate(96.0, 10_080, reset)); + + let windows = live_windows(&snap); + let weekly = windows + .iter() + .find(|window| window.id == "weekly") + .expect("weekly window"); + assert_eq!(weekly.used_percent, 20.0); + assert_eq!(weekly.label, "Weekly"); + let model = windows + .iter() + .find(|window| window.id == "model") + .expect("model window keeps its own run"); + assert_eq!(model.used_percent, 96.0); + } + + #[test] + fn a_named_third_window_runs_under_its_own_name() { + let now = Utc::now(); + let reset = now + Duration::hours(6); + let mut snap = snapshot("opencodego", "person@example.com", now, 34.0, reset); + snap.tertiary = Some(rate(57.0, 43_200, reset)); + snap.tertiary_label = Some("Monthly".into()); + + let windows = live_windows(&snap); + let monthly = windows + .iter() + .find(|window| window.id == "monthly") + .expect("monthly window"); + assert_eq!(monthly.label, "Monthly"); + assert_eq!(monthly.used_percent, 57.0); + } + #[test] fn tokens_per_percent_and_projection_require_enough_peak() { assert!(tokens_per_percent(Some(1_000_000), 4.0).is_none()); diff --git a/apps/desktop-tauri/src-tauri/src/usage_history.rs b/apps/desktop-tauri/src-tauri/src/usage_history.rs index 621296c5..536ce278 100644 --- a/apps/desktop-tauri/src-tauri/src/usage_history.rs +++ b/apps/desktop-tauri/src-tauri/src/usage_history.rs @@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize}; use crate::commands::{ProviderUsageSnapshot, RateWindowSnapshot}; -const STORE_VERSION: u8 = 2; +const STORE_VERSION: u8 = 3; const RETENTION_DAYS: i64 = 30; const MIN_SAMPLE_INTERVAL_MINUTES: i64 = 5; @@ -179,7 +179,14 @@ fn snapshot_windows(snapshot: &ProviderUsageSnapshot) -> Vec push_window(&mut windows, "model", Some("Model"), window); } if let Some(window) = snapshot.tertiary.as_ref() { - push_window(&mut windows, "tertiary", Some("API"), window); + // The window names itself when it can (OpenCode Go "Monthly"); "API" is + // the historical fallback from when Cursor was the only tertiary. + push_window( + &mut windows, + "tertiary", + snapshot.tertiary_label.as_deref().or(Some("API")), + window, + ); } for extra in &snapshot.extra_rate_windows { push_window(&mut windows, &extra.id, Some(&extra.title), &extra.window); @@ -273,10 +280,44 @@ fn load_store() -> UsageHistoryStore { let Some(path) = persistence_path() else { return UsageHistoryStore::default(); }; - fs::read(&path) + let mut store: UsageHistoryStore = fs::read(&path) .ok() .and_then(|bytes| serde_json::from_slice(&bytes).ok()) - .unwrap_or_default() + .unwrap_or_default(); + migrate_store(&mut store); + store +} + +/// A window's series id comes from the label it carried when it was recorded, +/// so renaming a label starts a second series and cuts the chart in two. Two +/// OpenCode Go renames land together: the rolling window now states its 5-hour +/// span, and the monthly window keeps its own name instead of the "API" +/// fallback left over from when Cursor was the only provider with a third +/// window. Points recorded under the old ids are relabelled in place so the +/// retained 30 days stay one line each. +fn migrate_store(store: &mut UsageHistoryStore) { + if store.version >= 3 { + return; + } + for (key, points) in store.series.iter_mut() { + if !key.starts_with("opencodego:") { + continue; + } + for point in points.iter_mut() { + for window in &mut point.windows { + let renamed = match window.id.as_str() { + "rolling" => Some(("rolling-5h", "Rolling (5h)")), + "api" => Some(("monthly", "Monthly")), + _ => None, + }; + if let Some((id, label)) = renamed { + window.id = id.to_string(); + window.label = label.to_string(); + } + } + } + } + store.version = STORE_VERSION; } fn persist_store(store: &UsageHistoryStore) { @@ -309,6 +350,139 @@ mod tests { assert_eq!(normalize_id(" API "), "api"); } + fn window_snapshot(used_percent: f64) -> RateWindowSnapshot { + RateWindowSnapshot { + used_percent, + remaining_percent: 100.0 - used_percent, + window_minutes: None, + resets_at: None, + reset_description: None, + is_exhausted: false, + reserve_percent: None, + reserve_description: None, + reserve_will_last_to_reset: false, + reserve_eta_seconds: None, + } + } + + #[test] + fn a_named_tertiary_window_charts_under_its_own_name() { + // OpenCode Go's third window is Monthly, not Cursor's API. + let mut snapshot = ProviderUsageSnapshot { + provider_id: "opencodego".into(), + display_name: "OpenCode Go".into(), + primary: window_snapshot(34.0), + primary_label: Some("Rolling (5h)".into()), + secondary: Some(window_snapshot(32.0)), + secondary_label: Some("Weekly".into()), + model_specific: None, + tertiary: Some(window_snapshot(57.0)), + tertiary_label: Some("Monthly".into()), + extra_rate_windows: Vec::new(), + inactive_rate_windows: Vec::new(), + promo_signals: Vec::new(), + reset_credits_available: None, + cost: None, + plan_name: None, + account_email: None, + source_label: "web".into(), + updated_at: "2026-08-04T00:00:00Z".into(), + error: None, + pace: None, + account_organization: None, + tray_status_label: None, + account_id: None, + account_label: None, + account_tint: None, + fetch_duration_ms: None, + wayfinder_usage: None, + }; + + let windows = snapshot_windows(&snapshot); + let named: Vec<_> = windows + .iter() + .map(|window| (window.id.as_str(), window.label.as_str())) + .collect(); + assert_eq!( + named, + vec![ + ("rolling-5h", "Rolling (5h)"), + ("weekly", "Weekly"), + ("monthly", "Monthly"), + ] + ); + + // An unnamed tertiary keeps the historical fallback. + snapshot.tertiary_label = None; + let windows = snapshot_windows(&snapshot); + assert_eq!(windows[2].id, "api"); + } + + #[test] + fn relabelled_opencode_go_windows_keep_one_series() { + // Recorded under the pre-rename ids; charting them as separate series + // would break the retained 30 days into two lines each. + let mut store = UsageHistoryStore { + version: 2, + series: HashMap::new(), + }; + store.series.insert( + scope_key("opencodego", None, None, None), + vec![UsageHistoryPoint { + recorded_at: "2026-08-01T00:00:00Z".into(), + windows: vec![ + UsageHistoryWindow { + id: "rolling".into(), + label: "Rolling".into(), + used_percent: 34.0, + }, + UsageHistoryWindow { + id: "weekly".into(), + label: "Weekly".into(), + used_percent: 32.0, + }, + UsageHistoryWindow { + id: "api".into(), + label: "API".into(), + used_percent: 57.0, + }, + ], + }], + ); + let untouched = vec![UsageHistoryPoint { + recorded_at: "2026-08-01T00:00:00Z".into(), + windows: vec![UsageHistoryWindow { + id: "api".into(), + label: "API".into(), + used_percent: 12.0, + }], + }]; + store + .series + .insert(scope_key("cursor", None, None, None), untouched.clone()); + + migrate_store(&mut store); + + let migrated = &store.series[&scope_key("opencodego", None, None, None)][0].windows; + assert_eq!( + migrated + .iter() + .map(|window| (window.id.as_str(), window.label.as_str())) + .collect::>(), + vec![ + ("rolling-5h", "Rolling (5h)"), + ("weekly", "Weekly"), + ("monthly", "Monthly"), + ] + ); + // Cursor's API window is a real API allowance and must not be renamed. + assert_eq!( + store.series[&scope_key("cursor", None, None, None)], + untouched + ); + assert_eq!(store.version, STORE_VERSION); + } + fn series_with( entries: &[(&str, Option<&str>, &str)], ) -> HashMap> { diff --git a/apps/desktop-tauri/src/lib/capacityPresentation.test.ts b/apps/desktop-tauri/src/lib/capacityPresentation.test.ts index 0a3ce2f0..0f63d92e 100644 --- a/apps/desktop-tauri/src/lib/capacityPresentation.test.ts +++ b/apps/desktop-tauri/src/lib/capacityPresentation.test.ts @@ -261,6 +261,30 @@ describe("capacityPresentation", () => { expect(meters.companions[0].window.usedPercent).toBe(20); }); + it("keeps OpenCode Go weekly beside its rolling and monthly ceilings", () => { + // The three windows are one plan: a quiet weekly must not drop out and + // leave a gap between the rolling hero and the monthly lane. + const meters = glanceMeters( + provider({ + providerId: "opencodego", + displayName: "OpenCode Go", + primary: window(34), + primaryLabel: "Rolling (5h)", + secondary: window(32), + secondaryLabel: "Weekly", + tertiary: window(57), + tertiaryLabel: "Monthly", + }), + ); + expect(meters.companions.map((meter) => meter.label)).toEqual([ + "Weekly", + "Monthly", + ]); + expect(meters.companions.map((meter) => meter.window.usedPercent)).toEqual([ + 32, 57, + ]); + }); + it("reports glance status from constraining pressure", () => { expect(providerGlanceStatus(provider({ error: "nope" }))).toBe("error"); expect( diff --git a/apps/desktop-tauri/src/lib/capacityPresentation.ts b/apps/desktop-tauri/src/lib/capacityPresentation.ts index c8ab90dc..7221df0c 100644 --- a/apps/desktop-tauri/src/lib/capacityPresentation.ts +++ b/apps/desktop-tauri/src/lib/capacityPresentation.ts @@ -179,13 +179,25 @@ export function constrainingWindow( return best; } +/** + * Lanes that define a plan alongside its hero, listed in display order and + * shown on overview however quiet they are: + * - Cursor's Auto and API are distinct allowances users need to compare. + * - Claude's Weekly sits beside the 5-hour session; both cap the subscription. + * - OpenCode Go bills against rolling, weekly, and monthly ceilings at once, so + * hiding weekly leaves a gap between the two windows that do show. + */ +const PINNED_COMPANION_IDS: Record = { + cursor: ["secondary", "extra-cursor-api"], + claude: ["secondary"], + opencodego: ["secondary", "tertiary"], +}; + /** * Overview glance model: primary plan pool as hero, plus compact companion - * lanes. Cursor always shows its reported Auto and API lanes because they are - * distinct allowances users need to compare. Claude always shows Weekly beside - * its 5-hour session because both limits define the subscription. Other - * providers keep the single hottest materially constrained lane. Clicking - * never toggles meters — detail mode lists every window. + * lanes. Providers in `PINNED_COMPANION_IDS` always show their defining lanes; + * every other provider keeps the single hottest materially constrained lane. + * Clicking never toggles meters — detail mode lists every window. */ export function glanceMeters(provider: ProviderUsageSnapshot): GlanceMeters { const primary: ConstrainingWindow = { @@ -195,17 +207,16 @@ export function glanceMeters(provider: ProviderUsageSnapshot): GlanceMeters { }; const candidates = nonPrimaryWindows(provider); - if (provider.providerId === "cursor") { - const cursorCompanions = [ - candidates.find((candidate) => candidate.id === "secondary"), - candidates.find((candidate) => candidate.id === "extra-cursor-api"), - ].filter((candidate): candidate is ConstrainingWindow => Boolean(candidate)); - return { primary, companions: cursorCompanions }; - } - - if (provider.providerId === "claude") { - const weekly = candidates.find((candidate) => candidate.id === "secondary"); - return { primary, companions: weekly ? [weekly] : [] }; + const pinned = PINNED_COMPANION_IDS[provider.providerId]; + if (pinned) { + return { + primary, + companions: pinned + .map((id) => candidates.find((candidate) => candidate.id === id)) + .filter((candidate): candidate is ConstrainingWindow => + Boolean(candidate), + ), + }; } let companion: ConstrainingWindow | null = null; diff --git a/apps/desktop-tauri/src/styles.css b/apps/desktop-tauri/src/styles.css index 21b49e2d..342a22d8 100644 --- a/apps/desktop-tauri/src/styles.css +++ b/apps/desktop-tauri/src/styles.css @@ -1341,10 +1341,13 @@ body:has(.taskbar-flyout-frame) #root { min-width: 0; } +/* Name first, then any chips it owns. `space-between` would have parked a lone + chip on the right and a second one mid-row, so two rows of the same provider + never lined up; the strip marker instead claims the right edge on its own. */ .taskbar-flyout__provider-topline { display: flex; align-items: center; - justify-content: space-between; + justify-content: flex-start; gap: 8px; flex-wrap: wrap; } @@ -1359,9 +1362,11 @@ body:has(.taskbar-flyout-frame) #root { white-space: nowrap; } -/* Secondary to banked-resets: identity cue only, keep it quieter and smaller. */ +/* Identity cue only, so it stays the quietest chip — but it holds the right + edge, which is the one position that repeats in the same place every row. */ .taskbar-flyout__on-strip { flex-shrink: 0; + margin-left: auto; font-size: 9px; font-weight: 600; letter-spacing: 0.03em; @@ -1388,17 +1393,18 @@ body:has(.taskbar-flyout-frame) #root { font-weight: 600; } -/* Primary action cue: slightly larger than the On strip marker. */ +/* Action cue: sized close to the strip marker so the row reads as one line of + chips, and kept ahead of it on tint rather than on size. */ .taskbar-flyout__reset-credit { flex-shrink: 0; - padding: 2px 8px; + padding: 1px 6px; border: 1px solid rgba(128, 221, 255, 0.28); border-radius: 999px; background: rgba(91, 201, 238, 0.12); color: rgba(224, 247, 255, 0.94); - font-size: 11px; + font-size: 10px; font-weight: 600; - line-height: 15px; + line-height: 14px; } /* No banked resets: keep the indicator visible but quiet — nothing to act on. */ @@ -1433,9 +1439,12 @@ body:has(.taskbar-flyout-frame) #root { min-width: 0; } +/* The reset column holds a floor width so "4h 9m" and "3d 23h" end at the same + edge. Without it every row sized that column to its own text and the percent + beside it landed somewhere different on each line. */ .taskbar-flyout__meter-meta { display: grid; - grid-template-columns: minmax(0, 1fr) auto auto; + grid-template-columns: minmax(0, 1fr) auto minmax(52px, auto); align-items: baseline; gap: 7px; margin-bottom: 3px; @@ -1495,6 +1504,7 @@ body:has(.taskbar-flyout-frame) #root { font-size: 10px; line-height: 13px; font-variant-numeric: tabular-nums; + text-align: right; white-space: nowrap; } diff --git a/apps/desktop-tauri/src/surfaces/TaskbarFlyout.tsx b/apps/desktop-tauri/src/surfaces/TaskbarFlyout.tsx index 4144522d..5f2489a1 100644 --- a/apps/desktop-tauri/src/surfaces/TaskbarFlyout.tsx +++ b/apps/desktop-tauri/src/surfaces/TaskbarFlyout.tsx @@ -123,10 +123,10 @@ function ProviderRow({ provider, showAccount, hideEmail, onStrip, showAsUsed, no
{provider.displayName} + Unavailable {onStrip && ( On strip )} - Unavailable
{accountName && (
@@ -153,9 +153,8 @@ function ProviderRow({ provider, showAccount, hideEmail, onStrip, showAsUsed, no
{provider.displayName} - {onStrip && ( - On strip - )} + {/* Banked resets sit next to the name they belong to; the strip marker + holds the right edge so it lines up down the whole flyout. */} {resetCredits != null && ( )} + {onStrip && ( + On strip + )}
{accountName && (
diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 07201d05..0f0ba92a 100755 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -116,13 +116,6 @@ tokio-test = "0.4" tempfile = "3" mockito = "1.4" -[profile.release] -opt-level = 3 -lto = true -codegen-units = 1 -strip = true -panic = "abort" - [[bin]] name = "codexbar" path = "src/main.rs" diff --git a/rust/src/notifications.rs b/rust/src/notifications.rs index 159ef8d9..f3112c29 100755 --- a/rust/src/notifications.rs +++ b/rust/src/notifications.rs @@ -485,7 +485,12 @@ impl NotificationManager { used_percent: f64, settings: &Settings, ) { - if !settings.show_notifications || !matches!(window, "session" | "weekly") { + // A model pool is a hard ceiling on the work you can do right now — a + // maxed Opus allowance stops Opus — so it warns like the session and + // weekly windows. Monthly stays out on purpose: it moves slowly enough + // that crossing a threshold mid-cycle is normal, not news. See + // `monthly_and_unknown_windows_never_raise_usage_toasts`. + if !settings.show_notifications || !matches!(window, "session" | "weekly" | "model") { return; } @@ -1716,6 +1721,24 @@ mod tests { ); } + #[test] + fn a_model_pool_can_warn_like_the_windows_beside_it() { + // Claude's Opus allowance was filtered out of `check_and_notify`, so it + // could reach 99% in silence while the weekly window beside it warned. + let mut manager = NotificationManager::new_armed(); + let settings = Settings::default(); + + // First reading is the baseline and never warns, by design. + manager.check_and_notify(ProviderId::Claude, None, "model", 20.0, &settings); + // A crossing waits for a second reading to confirm it, like every other + // window: one anomalous refresh must not raise a toast on its own. + manager.check_and_notify(ProviderId::Claude, None, "model", 92.0, &settings); + assert_eq!(manager.toasts_shown, 0); + + manager.check_and_notify(ProviderId::Claude, None, "model", 96.0, &settings); + assert_eq!(manager.toasts_shown, 1); + } + #[test] fn threshold_alerts_are_isolated_per_account() { let mut manager = NotificationManager::new_armed(); diff --git a/rust/src/providers/devin/mod.rs b/rust/src/providers/devin/mod.rs index bc230d55..272f71b6 100644 --- a/rust/src/providers/devin/mod.rs +++ b/rust/src/providers/devin/mod.rs @@ -125,11 +125,25 @@ fn normalized_org(raw: &str) -> String { } fn snapshot_from_quota(value: &Value, org: &str) -> UsageSnapshot { - let daily = percent(value, &["daily_percentage", "dailyPercentage"]) - .unwrap_or_else(|| percent(value, &["used_percent", "usedPercent"]).unwrap_or(0.0)); + // Devin reports each window as either whole percentages (`23` = 23%) or + // fractions of the limit (`0.23` = 23%), and one value cannot tell them + // apart on its own. Resolve the scale once from every reported window, so a + // response holding `0.4` next to `32` reads the `0.4` as 0.4% rather than + // rescaling it to 40% — the per-value rule the other providers dropped. + let raw_daily = reported_percent(value, &["daily_percentage", "dailyPercentage"]) + .or_else(|| reported_percent(value, &["used_percent", "usedPercent"])); + let raw_weekly = reported_percent(value, &["weekly_percentage", "weeklyPercentage"]); + let fraction_scale = + crate::core::detect_fraction_scale(raw_daily.into_iter().chain(raw_weekly)); + + // A used/limit pair is already a true percentage and never gets rescaled. + let daily = raw_daily + .map(|raw| crate::core::to_percent(raw, fraction_scale)) + .or_else(|| ratio_percent(value)) + .unwrap_or(0.0); let mut snapshot = UsageSnapshot::new(RateWindow::new(daily)).with_organization(org.to_string()); - if let Some(weekly) = percent(value, &["weekly_percentage", "weeklyPercentage"]) { + if let Some(weekly) = raw_weekly.map(|raw| crate::core::to_percent(raw, fraction_scale)) { snapshot = snapshot.with_secondary(RateWindow::new(weekly)); } snapshot @@ -143,12 +157,17 @@ fn fetch_result_from_quota(value: &Value, org: &str) -> ProviderFetchResult { result } -fn percent(value: &Value, keys: &[&str]) -> Option { - for key in keys { - if let Some(v) = value.get(*key).and_then(Value::as_f64) { - return Some(if v < 1.0 { v * 100.0 } else { v }); - } - } +/// A window's reported value exactly as Devin sent it, before any scaling. +fn reported_percent(value: &Value, keys: &[&str]) -> Option { + keys.iter() + .find_map(|key| value.get(*key).and_then(Value::as_f64)) + .filter(|value| value.is_finite()) +} + +/// Percent derived from a used/limit pair. Only the daily window falls back to +/// this: it describes the account as a whole, so reporting it as the weekly +/// window too would invent a second ceiling out of the same number. +fn ratio_percent(value: &Value) -> Option { let used = ["used", "usage", "used_count", "usedCount", "consumed"] .iter() .find_map(|k| value.get(*k).and_then(Value::as_f64)); @@ -198,6 +217,40 @@ mod tests { assert_eq!(snapshot.primary.used_percent, 1.0); } + #[test] + fn a_whole_percent_sibling_settles_the_scale_for_the_response() { + // 32 can only be a percentage, which proves this response is not on the + // fraction scale — so 0.4 is 0.4% used, not 40%. + let snapshot = snapshot_from_quota( + &serde_json::json!({"daily_percentage": 0.4, "weekly_percentage": 32.0}), + "org/demo", + ); + assert!((snapshot.primary.used_percent - 0.4).abs() < 0.001); + assert!((snapshot.secondary.unwrap().used_percent - 32.0).abs() < 0.001); + } + + #[test] + fn fractions_still_scale_together() { + let snapshot = snapshot_from_quota( + &serde_json::json!({"daily_percentage": 0.4, "weekly_percentage": 0.32}), + "org/demo", + ); + assert!((snapshot.primary.used_percent - 40.0).abs() < 0.001); + assert!((snapshot.secondary.unwrap().used_percent - 32.0).abs() < 0.001); + } + + #[test] + fn a_used_limit_pair_is_not_repeated_as_a_weekly_window() { + // The pair describes the account overall. Reporting it as weekly too + // invented a second ceiling from the same number. + let snapshot = snapshot_from_quota( + &serde_json::json!({"used": 25.0, "limit": 100.0}), + "org/demo", + ); + assert!((snapshot.primary.used_percent - 25.0).abs() < 0.001); + assert!(snapshot.secondary.is_none()); + } + #[test] fn parses_extra_usage_balance() { let result = fetch_result_from_quota( diff --git a/rust/src/providers/opencodego/mod.rs b/rust/src/providers/opencodego/mod.rs index 1f782ff7..c2ebb37a 100644 --- a/rust/src/providers/opencodego/mod.rs +++ b/rust/src/providers/opencodego/mod.rs @@ -31,7 +31,7 @@ impl OpenCodeGoProvider { metadata: ProviderMetadata { id: ProviderId::OpenCodeGo, display_name: "OpenCode Go", - session_label: "Rolling", + session_label: "Rolling (5h)", weekly_label: "Weekly", supports_opus: false, supports_credits: false,