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
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ The Windows server token now rotates after unsafe exposure. Concurrent and corru
- **A leaked `serve.token` is rotated on Windows, not just Unix.** After SBS-953, a world-readable token was replaced on Unix, but Windows still tightened the DACL and reused the same secret. The ACL is now inspected before tightening; if anyone other than the current user, SYSTEM, or Administrators can read the file, the token is replaced. Closes SBS-1043.

### Fixed
- **MCP `get_status` now ranks Cursor the way the strip does.** After SBS-1055, `remaining_percent` used generic exhausted-first ranking over primary/secondary/tertiary, so a hotter Plan could hide Auto, and the widget snapshot still omitted `cursor-api` / on-demand while the docs claimed strip parity. Cursor now uses `cursorStripWindow` (hottest Auto/API with room, then on-demand, Plan last), those extras persist on the snapshot, and the multi-account seat picker compares the same window. Closes SBS-1076.
- **Timed-out Kiro, Augment, and Vertex CLI fetches no longer leave orphaned children.** Those providers spawned `tokio::process::Command` without `kill_on_drop`, so a desktop refresh timeout dropped the `Child` and the CLI kept running. Fetch-path commands now kill on cancel; Augment's inner 15s deadline also kill+waits like `command_runner`. Closes SBS-1078.
- **Frontend tests now catch accessibility regressions automatically.** A shared axe assertion checks representative quota cards, mini charts, and update banners in the existing Frontend CI job. Color contrast remains outside jsdom coverage because it requires a rendered browser. Fixes #222.
- **`usage --all-accounts` now fetches every configured Codex and Claude account.** Account fetches run with bounded concurrency, preserve configured order, and report failures independently. Text and JSON identify each configured account while the default output remains unchanged. Fixes #274.
Expand All @@ -20,7 +21,7 @@ The Windows server token now rotates after unsafe exposure. Concurrent and corru
- **Codex latest-session cost follows transcript time.** Copying or touching an older rollout no longer makes it replace a newer session in local cost summaries. Fixes #271.
- **A corrupt `window_geometry.json` no longer wipes other windows' saved positions.** SBS-1024 locked the persist so two surfaces could not drop each other's keys, but a file that would not read or parse still loaded as empty defaults, and the next save replaced the whole file with only the window that just moved. Persist now refuses that write — the same fail-closed rule API keys already use — instead of rewriting siblings to an empty store. Closes SBS-1041.
- **Loading settings no longer rewrites another install's start-at-login command.** Every `Settings::load` repaired `HKCU\...\Run\Ceiling` whenever the value was not the quoted path of this process. A portable CLI or a second tree therefore replaced the installed desktop's startup entry, and any extra arguments were stripped. Repair now runs only when this process owns that entry — the same intended exe, or a stale `codexbar-cli.exe` / `codexbar-desktop.exe` sibling in the same directory — and leaves custom arguments and other trees alone. Closes SBS-1053.
- **MCP `get_status` no longer hides an exhausted Weekly behind a healthy session.** Top-level `remaining_percent` copied only `usage.primary`, so a Claude/Codex 5-hour window with room made the advertised cap-check sink look fine while Weekly was already at 100%. It now uses the same constraining-window ranking as the desktop strip across primary, secondary, and tertiary (exhausted first, then highest used %). Closes SBS-1055.
- **MCP `get_status` no longer hides an exhausted Weekly behind a healthy session.** Top-level `remaining_percent` copied only `usage.primary`, so a Claude/Codex 5-hour window with room made the advertised cap-check sink look fine while Weekly was already at 100%. Claude/Codex now rank primary, secondary, and tertiary (exhausted first, then highest used %). Cursor's parallel Auto/API path landed in SBS-1076. Closes SBS-1055.
- **Remembered window positions no longer drop a sibling when two surfaces save at once.** `window_geometry.json` was updated with an unlocked read-modify-write, so moving Settings while the float bar or Pop Out also wrote could replace the file with a snapshot that had never seen the other key. Geometry persist now holds the same cross-process state lock as settings and credentials. Closes SBS-1024.
- **`codexbar` with no subcommand now runs `usage`.** CLI.md and `--help` already called usage the default command, but a bare `codexbar` printed an error asking for an explicit subcommand. It now does what those docs said. Closes SBS-1026.
- **A corrupt `settings.json` is no longer renamed to `.bak` without the state lock.** SBS-954 moved an unparseable file aside so the next save could not overwrite it, but `Settings::load` did that rename before taking the lock. A concurrent `try_update` that had already written a good repair could then have that repair moved to `settings.json.bak`. The unlocked load now only parses; if the file is corrupt (or still carries embedded credentials) it takes the lock, re-reads, and quarantines only then. Closes SBS-1029.
Expand Down
139 changes: 137 additions & 2 deletions apps/desktop-tauri/src-tauri/src/commands/providers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -684,8 +684,8 @@ pub(super) fn most_constrained_per_provider(
.find(|existing| existing.provider_id == snapshot.provider_id)
{
Some(existing) => {
let existing_used = existing.primary.used_percent;
let used = snapshot.primary.used_percent;
let existing_used = snapshot_constraint_used_percent(existing);
let used = snapshot_constraint_used_percent(snapshot);
// Ties resolve on account id so the strip does not flicker
// between accounts as readings land in different orders.
let replace = used > existing_used
Expand Down Expand Up @@ -757,6 +757,14 @@ fn widget_entry_from_usage_snapshot(
if let Some(tertiary) = snap.tertiary.as_ref() {
entry = entry.with_tertiary(rate_window_from_snapshot(tertiary));
}
if !snap.extra_rate_windows.is_empty() {
entry = entry.with_extra_rate_windows(
snap.extra_rate_windows
.iter()
.map(named_window_from_snapshot)
.collect(),
);
}
if let Some(email) = snap.account_email.clone() {
entry = entry.with_account_email(email);
}
Expand All @@ -776,6 +784,63 @@ fn widget_entry_from_usage_snapshot(
Some(entry)
}

/// Used % of the window the desktop strip would show for this snapshot.
///
/// Claude/Codex keep comparing primary so existing seat-picker tests stay
/// stable. Cursor uses `cursorStripWindow` (Auto / API / on-demand), not Plan.
fn snapshot_constraint_used_percent(snapshot: &ProviderUsageSnapshot) -> f64 {
if snapshot.provider_id != "cursor" {
return snapshot.primary.used_percent;
}
let owned = owned_windows_from_snapshot(snapshot);
codexbar::core::constraining_rate_window(
ProviderId::Cursor,
Some(&owned.primary),
owned.secondary.as_ref(),
owned.tertiary.as_ref(),
&owned.extras,
)
.map(|window| window.used_percent)
.unwrap_or(snapshot.primary.used_percent)
}

struct OwnedRankWindows {
primary: RateWindow,
secondary: Option<RateWindow>,
tertiary: Option<RateWindow>,
extras: Vec<codexbar::core::NamedRateWindow>,
}

fn owned_windows_from_snapshot(snapshot: &ProviderUsageSnapshot) -> OwnedRankWindows {
OwnedRankWindows {
primary: rate_window_from_snapshot(&snapshot.primary),
secondary: snapshot.secondary.as_ref().map(rate_window_from_snapshot),
tertiary: snapshot.tertiary.as_ref().map(rate_window_from_snapshot),
extras: snapshot
.extra_rate_windows
.iter()
.map(named_window_from_snapshot)
.collect(),
}
}

fn named_window_from_snapshot(extra: &NamedRateWindowSnapshot) -> codexbar::core::NamedRateWindow {
let mut named = codexbar::core::NamedRateWindow::new(
extra.id.clone(),
extra.title.clone(),
rate_window_from_snapshot(&extra.window),
);
if let Some(amount) = extra.amount.as_ref() {
let mut money =
codexbar::core::WindowAmount::new(amount.used, amount.currency_code.clone());
if let Some(limit) = amount.limit {
money = money.with_limit(limit);
}
named = named.with_amount(money);
}
named
}

fn rate_window_from_snapshot(window: &RateWindowSnapshot) -> RateWindow {
let resets_at = window.resets_at.as_deref().and_then(|raw| {
chrono::DateTime::parse_from_rfc3339(raw)
Expand Down Expand Up @@ -1705,4 +1770,74 @@ mod widget_snapshot_tests {
let entry = widget_entry_from_usage_snapshot(&snap).expect("entry");
assert_eq!(entry.primary.expect("measured primary").used_percent, 0.0);
}

/// SBS-1076: the strip ranks cursor-api / on-demand, but the widget snapshot
/// dropped extras so MCP could not match cursorStripWindow.
#[test]
fn widget_entry_persists_cursor_api_and_on_demand() {
let metadata = instantiate_provider(ProviderId::Cursor).metadata().clone();
let usage = UsageSnapshot::new(RateWindow::new(95.0))
.with_secondary(RateWindow::new(55.0))
.with_extra_rate_window("cursor-api", "API", RateWindow::new(12.0));
let mut usage = usage;
usage.extra_rate_windows.push(
codexbar::core::NamedRateWindow::new(
"cursor-on-demand",
"On-demand",
RateWindow::new(56.0),
)
.with_amount(codexbar::core::WindowAmount::new(1_002.16, "USD").with_limit(1_800.0)),
);
let result = ProviderFetchResult::new(usage, "oauth");
let snap = ProviderUsageSnapshot::from_fetch_result(ProviderId::Cursor, &metadata, &result);
let entry = widget_entry_from_usage_snapshot(&snap).expect("entry");

let ids: Vec<&str> = entry
.extra_rate_windows
.iter()
.map(|extra| extra.id.as_str())
.collect();
assert_eq!(ids, vec!["cursor-api", "cursor-on-demand"]);
let on_demand = entry
.extra_rate_windows
.iter()
.find(|extra| extra.id == "cursor-on-demand")
.expect("on-demand");
let amount = on_demand.amount.as_ref().expect("amount");
assert_eq!(amount.used, 1002.16);
assert_eq!(amount.limit, Some(1800.0));
// Spend already started: the strip surfaces on-demand even while Auto
// still has room. Persist has to keep the amount so that ranking can.
assert_eq!(
entry.constraining_rate_window().map(|w| w.used_percent),
Some(56.0),
"on-demand spend binds the strip window"
);
}

#[test]
fn widget_entry_cursor_ranking_keeps_auto_when_on_demand_is_unused() {
let metadata = instantiate_provider(ProviderId::Cursor).metadata().clone();
let usage = UsageSnapshot::new(RateWindow::new(95.0))
.with_secondary(RateWindow::new(55.0))
.with_extra_rate_window("cursor-api", "API", RateWindow::new(12.0));
let mut usage = usage;
usage.extra_rate_windows.push(
codexbar::core::NamedRateWindow::new(
"cursor-on-demand",
"On-demand",
RateWindow::new(0.0),
)
.with_amount(codexbar::core::WindowAmount::new(0.0, "USD").with_limit(1_800.0)),
);
let result = ProviderFetchResult::new(usage, "oauth");
let snap = ProviderUsageSnapshot::from_fetch_result(ProviderId::Cursor, &metadata, &result);
let entry = widget_entry_from_usage_snapshot(&snap).expect("entry");

assert_eq!(
entry.constraining_rate_window().map(|w| w.used_percent),
Some(55.0),
"unused on-demand must not let Plan outrank Auto"
);
}
}
58 changes: 58 additions & 0 deletions apps/desktop-tauri/src-tauri/src/commands/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -949,6 +949,64 @@ fn the_taskbar_strip_does_not_flicker_between_tied_accounts() {
);
}

fn cursor_account_snapshot(
account_id: &str,
plan: f64,
auto: f64,
api: Option<f64>,
) -> ProviderUsageSnapshot {
let metadata = instantiate_provider(ProviderId::Cursor).metadata().clone();
let mut usage = codexbar::core::UsageSnapshot::new(codexbar::core::RateWindow::new(plan))
.with_secondary(codexbar::core::RateWindow::new(auto));
if let Some(api) = api {
usage =
usage.with_extra_rate_window("cursor-api", "API", codexbar::core::RateWindow::new(api));
}
let result = ProviderFetchResult {
usage,
cost: None,
wayfinder_usage: None,
source_label: "oauth".to_string(),
};
let mut snapshot =
ProviderUsageSnapshot::from_fetch_result(ProviderId::Cursor, &metadata, &result);
snapshot.account_id = Some(account_id.to_string());
snapshot
}

/// SBS-1076: picking by Plan used% chose the seat whose blend was hotter even
/// when Auto still had room. The strip ranks Auto/API, so the seat picker must.
#[test]
fn cursor_seat_picker_uses_strip_window_not_plan() {
let cached = vec![
cursor_account_snapshot("acct-plan-hot", 95.0, 10.0, Some(8.0)),
cursor_account_snapshot("acct-auto-hot", 40.0, 70.0, Some(20.0)),
];

let chosen = super::most_constrained_per_provider(&cached);
assert_eq!(chosen.len(), 1);
assert_eq!(
chosen[0].account_id.as_deref(),
Some("acct-auto-hot"),
"hotter Auto must win over hotter Plan"
);
}

#[test]
fn cursor_seat_picker_prefers_api_room_over_exhausted_auto_on_other_seat() {
let cached = vec![
cursor_account_snapshot("acct-auto-maxed", 40.0, 100.0, Some(15.0)),
cursor_account_snapshot("acct-auto-open", 90.0, 20.0, Some(100.0)),
];

let chosen = super::most_constrained_per_provider(&cached);
assert_eq!(
chosen[0].account_id.as_deref(),
Some("acct-auto-open"),
"Auto with room (20%) is the strip window; the other seat's API 15% is cooler"
);
}

#[test]
fn the_taskbar_strip_skips_providers_that_failed_to_fetch() {
let mut errored = account_snapshot("acct-work", 91.0);
Expand Down
4 changes: 2 additions & 2 deletions docs/CLI.md
Original file line number Diff line number Diff line change
Expand Up @@ -230,9 +230,9 @@ Tools:
| Tool | Source | Notes |
|---|---|---|
| `list_providers` | widget snapshot + settings | Quota cache presence and whether local spend scanning is supported. |
| `get_usage` | widget snapshot | Remaining quota windows. `period_cost_usd` is the provider's billed / current-period `CostSnapshot.used`, with `cost_period` as the provider's period label (for example `Monthly`). It is **not** this conversation's spend. |
| `get_usage` | widget snapshot | Remaining quota windows, including `extra_rate_windows` (Cursor `cursor-api` and `cursor-on-demand`). `period_cost_usd` is the provider's billed / current-period `CostSnapshot.used`, with `cost_period` as the provider's period label (for example `Monthly`). It is **not** this conversation's spend. |
| `get_spend` | local Codex / Claude / Grok logs | Estimated API-value spend for today, 7 days, and 30 days. Not a bill. |
| `get_status` | snapshot + local logs | Compact remaining-quota plus `today_spend`. `remaining_percent` is the constraining window across primary/secondary/tertiary (exhausted first, then highest used %), not `usage.primary` alone. `usage` is the same object as `get_usage` (including `period_cost_usd`). `today_spend` is local estimated log spend for today. |
| `get_status` | snapshot + local logs | Compact remaining-quota plus `today_spend`. `remaining_percent` is the same window the desktop strip shows — not `usage.primary` alone. Claude/Codex rank primary/secondary/tertiary (exhausted first, then highest used %). Cursor uses `cursorStripWindow`: hottest Auto/API with room, then on-demand when included lanes are gone or already billing, Plan only as fallback. `usage` is the same object as `get_usage` (including `extra_rate_windows` and `period_cost_usd`). `today_spend` is local estimated log spend for today. |

`session_cost_usd` is not emitted. Older builds stuffed billed period cost into that name.

Expand Down
Loading
Loading