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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

## [Windows] Unreleased

- **Claude**: show the plan renewal or expiry date from claude.ai billing under the quota rows when your cookie session supplies it, and make saved limit-reset credits, Extra usage (including a prepaid balance), and plan dates independently selectable under Visible usage items. Existing settings keep all three visible; Admin API spend is unaffected (upstream #4324, #4484).
- Devin: use Devin's official transparent hexagon mark for the provider icon in Settings and the CLI dashboard, instead of the settings-gear badge and the letter fallback (upstream #4458).
- Kimi (China): use the current Kimi Desktop session from its Local Storage (`%APPDATA%\kimi-desktop`) for web usage and monthly membership enrichment, and check that session again before each request, so a signed-out, replaced, or expired Desktop token is never sent or recovered from an older record (upstream #4416, #3536).
- Codex CLI: add opt-in `cost --identified-only` request subtotals across local and SSH (`--remote`) histories, deduplicating strong request identities and keeping partial results without estimating a combined price, plus `--group-by project` reports that show the partial-history notice even when no projects are listed (upstream #4447, #4472).
Expand Down
7 changes: 7 additions & 0 deletions apps/desktop-tauri/src-tauri/src/commands/bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,11 @@ pub struct SubscriptionMetadataSnapshot {
pub starts_at: Option<String>,
pub expires_at: Option<String>,
pub renews_at: Option<String>,
/// Date-only values are UTC midnight and must be formatted in UTC.
#[serde(default)]
pub renews_at_is_date_only: bool,
#[serde(default)]
pub expires_at_is_date_only: bool,
}

/// Display-only provider inventory. Redemption identifiers never cross the
Expand Down Expand Up @@ -692,6 +697,8 @@ impl ProviderUsageSnapshot {
starts_at: subscription.starts_at.map(|date| date.to_rfc3339()),
expires_at: subscription.expires_at.map(|date| date.to_rfc3339()),
renews_at: subscription.renews_at.map(|date| date.to_rfc3339()),
renews_at_is_date_only: subscription.renews_at_is_date_only,
expires_at_is_date_only: subscription.expires_at_is_date_only,
}
}),
source_label: result.source_label.clone(),
Expand Down
4 changes: 4 additions & 0 deletions apps/desktop-tauri/src-tauri/src/commands/provider_detail.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ pub struct ProviderDetail {
pub hidden_usage_item_ids: Vec<String>,
pub inventory: Vec<ProviderInventoryItemSnapshot>,
pub display_details: Vec<ProviderDisplayDetailSnapshot>,
/// Plan renewal / expiry dates for the Settings usage preview.
pub subscription: Option<SubscriptionMetadataSnapshot>,

// Cost / pace.
pub cost: Option<CostSnapshotBridge>,
Expand Down Expand Up @@ -147,6 +149,7 @@ pub(crate) fn build_provider_detail(
hidden_usage_item_ids: settings.hidden_usage_item_ids(id),
inventory: Vec::new(),
display_details: Vec::new(),
subscription: None,
cost: None,
pace: None,
open_ai_api_usage: None,
Expand Down Expand Up @@ -223,6 +226,7 @@ pub fn get_provider_detail(
detail.extra_rate_windows = snapshot.extra_rate_windows.clone();
detail.inventory = snapshot.inventory.clone();
detail.display_details = snapshot.display_details.clone();
detail.subscription = snapshot.subscription.clone();
detail.cost = snapshot.cost.clone();
detail.pace = snapshot.pace.clone();
detail.open_ai_api_usage = snapshot.open_ai_api_usage.clone();
Expand Down
113 changes: 113 additions & 0 deletions apps/desktop-tauri/src-tauri/src/commands/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1549,6 +1549,119 @@ fn usage_item_descriptors_skip_card_hidden_windows() {
);
}

fn claude_snapshot_with_sections(source_label: &str) -> ProviderUsageSnapshot {
let metadata = instantiate_provider(ProviderId::Claude).metadata().clone();
let result = ProviderFetchResult {
usage: codexbar::core::UsageSnapshot::new(codexbar::core::RateWindow::new(10.0)),
cost: Some(codexbar::core::CostSnapshot::new(0.0, "USD", "Extra usage").with_balance(5.0)),
wayfinder_usage: None,
open_ai_api_usage: None,
inventory: Vec::new(),
reset_credits: None,
display_details: Vec::new(),
source_label: source_label.to_string(),
has_successful_claude_cli_quota: false,
pace_authoritative: true,
account_identity: None,
last_good_owner: None,
};
let mut snapshot = ProviderUsageSnapshot::from_fetch_result(
ProviderId::Claude,
&metadata,
&result,
None,
None,
);
snapshot.inventory = vec![super::bridge::ProviderInventoryItemSnapshot {
id: "reset-credits".to_string(),
title: "Limit Reset Credits".to_string(),
available_count: 2,
next_expires_at: None,
}];
snapshot.subscription = Some(super::bridge::SubscriptionMetadataSnapshot {
starts_at: None,
expires_at: None,
renews_at: Some("2026-12-01T00:00:00+00:00".to_string()),
renews_at_is_date_only: true,
expires_at_is_date_only: false,
});
snapshot
}

#[test]
fn usage_item_descriptors_offer_independent_claude_sections() {
let snapshot = claude_snapshot_with_sections("web");
let items =
super::usage_item_descriptors(Some(&snapshot), &Settings::default(), ProviderId::Claude);
let sections: Vec<(&str, &str, bool, bool)> = items
.iter()
.filter(|item| item.id.starts_with("section:"))
.map(|item| {
(
item.id.as_str(),
item.title.as_str(),
item.available,
item.requires_cost_summary,
)
})
.collect();
assert_eq!(
sections,
[
("section:subscription", "Plan renewal / expiry", true, false),
(
"section:claude-reset-credits",
"Limit reset credits",
true,
false
),
("section:claude-extra-usage", "Extra usage", true, true),
]
);

// Admin API spend is not Extra usage and has no Extra usage choice.
let admin = claude_snapshot_with_sections("admin-api");
let items =
super::usage_item_descriptors(Some(&admin), &Settings::default(), ProviderId::Claude);
assert!(
!items
.iter()
.any(|item| item.id == "section:claude-extra-usage")
);

// The same sections are never offered for another provider.
let items =
super::usage_item_descriptors(Some(&snapshot), &Settings::default(), ProviderId::Codex);
assert!(!items.iter().any(|item| item.id.starts_with("section:")));
}

#[test]
fn usage_item_descriptors_keep_hidden_claude_sections_restorable() {
let mut settings = Settings::default();
settings.set_hidden_usage_item_ids(
ProviderId::Claude,
vec![
"section:claude-reset-credits".to_string(),
"section:unknown".to_string(),
],
);
let items = super::usage_item_descriptors(None, &settings, ProviderId::Claude);
let ids: Vec<(&str, &str, bool)> = items
.iter()
.map(|item| (item.id.as_str(), item.title.as_str(), item.available))
.collect();
assert_eq!(
ids,
[(
"section:claude-reset-credits",
"Limit reset credits (unavailable)",
false
)]
);
// Section IDs never become "metric:section:..." placeholders.
assert!(!items.iter().any(|item| item.id.starts_with("metric:")));
}

#[test]
fn pace_stage_serializes_to_snake_case_string() {
use codexbar::core::PaceStage;
Expand Down
107 changes: 102 additions & 5 deletions apps/desktop-tauri/src-tauri/src/commands/usage_items.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ use codexbar::settings::Language;
use serde::{Deserialize, Serialize};

/// Presentation descriptor for one quota metric or provider-emitted extra
/// usage row. This intentionally excludes inventory and transient detail
/// sections: Windows only exposes the metric rows already present in the
/// provider snapshot for this visibility lane.
/// usage row, a provider detail section, or a provider-owned `section:` card
/// block (Claude reset credits, Extra usage, plan dates). Other inventory
/// rows have no visibility choice.
///
/// Descriptor contract: every row the pane displays is either persisted in the
/// provider's hidden-usage-item list or emitted by the current provider
Expand All @@ -22,12 +22,75 @@ pub struct ProviderUsageItemSnapshot {
pub id: String,
pub title: String,
pub available: bool,
/// The row is also suppressed while Cost summary display is Hidden, so
/// the pane explains why checking it may show nothing.
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub requires_cost_summary: bool,
}

pub(crate) fn usage_item_id(raw_id: &str) -> String {
format!("{}{}", codexbar::settings::USAGE_ITEM_METRIC_PREFIX, raw_id)
}

/// Provider-specific by design (upstream 0.75.0 #4484): Claude's saved reset
/// credits, Extra usage / prepaid balance and plan dates are card sections
/// with their own visibility choice. Returns the title and whether the row is
/// also gated by Cost summary display.
fn claude_section_title(id: &str, lang: Language) -> Option<(String, bool)> {
use codexbar::settings::{
CLAUDE_EXTRA_USAGE_USAGE_ITEM_ID, CLAUDE_RESET_CREDITS_USAGE_ITEM_ID,
SUBSCRIPTION_USAGE_ITEM_ID,
};
let (key, requires_cost_summary) = match id {
SUBSCRIPTION_USAGE_ITEM_ID => (LocaleKey::UsageItemPlanRenewalExpiry, false),
CLAUDE_RESET_CREDITS_USAGE_ITEM_ID => (LocaleKey::ProviderLabelLimitResetCredits, false),
CLAUDE_EXTRA_USAGE_USAGE_ITEM_ID => (LocaleKey::ExtraUsage, true),
_ => return None,
};
Some((locale::get_text(lang, key), requires_cost_summary))
}

/// The Claude section IDs the current snapshot reports, in card order: plan
/// dates sit under the quota rows, then saved resets, then Extra usage.
fn claude_reported_sections(snapshot: &ProviderUsageSnapshot) -> Vec<&'static str> {
use codexbar::settings::{
CLAUDE_EXTRA_USAGE_USAGE_ITEM_ID, CLAUDE_RESET_CREDITS_USAGE_ITEM_ID,
SUBSCRIPTION_USAGE_ITEM_ID,
};
let mut ids = Vec::new();
if snapshot
.subscription
.as_ref()
.is_some_and(|s| s.renews_at.is_some() || s.expires_at.is_some())
{
ids.push(SUBSCRIPTION_USAGE_ITEM_ID);
}
if snapshot
.inventory
.iter()
.any(|item| item.id == CLAUDE_RESET_CREDITS_INVENTORY_ID)
{
ids.push(CLAUDE_RESET_CREDITS_USAGE_ITEM_ID);
}
if is_claude_extra_usage_cost(snapshot) {
ids.push(CLAUDE_EXTRA_USAGE_USAGE_ITEM_ID);
}
ids
}

/// Inventory ID the Claude provider assigns to saved limit resets.
pub(crate) const CLAUDE_RESET_CREDITS_INVENTORY_ID: &str = "reset-credits";

/// Claude's Extra usage / prepaid balance block. Admin API spend is a
/// separate cost and keeps showing regardless of this choice.
pub(crate) fn is_claude_extra_usage_cost(snapshot: &ProviderUsageSnapshot) -> bool {
snapshot
.cost
.as_ref()
.is_some_and(|cost| !cost.always_visible)
&& snapshot.source_label != "admin-api"
}

fn redacted_usage_item_title(title: &str, settings: &Settings) -> String {
PersonalInfoRedactor::hide_emails_in_text(Some(title), settings.hide_personal_info)
.unwrap_or_default()
Expand Down Expand Up @@ -109,6 +172,7 @@ pub(crate) fn usage_item_descriptors(
id,
title: redacted_usage_item_title(&detail.title, settings),
available: true,
requires_cost_summary: false,
});
}
}
Expand All @@ -129,6 +193,7 @@ pub(crate) fn usage_item_descriptors(
settings,
),
available: false,
requires_cost_summary: false,
});
}
}
Expand Down Expand Up @@ -167,6 +232,7 @@ pub(crate) fn usage_item_descriptors(
id,
title: item_title,
available,
requires_cost_summary: false,
});
}
};
Expand Down Expand Up @@ -203,8 +269,10 @@ pub(crate) fn usage_item_descriptors(
}
}

for id in hidden {
if id.starts_with(codexbar::settings::USAGE_ITEM_DETAIL_SECTION_PREFIX) {
for id in &hidden {
if id.starts_with(codexbar::settings::USAGE_ITEM_DETAIL_SECTION_PREFIX)
|| id.starts_with(codexbar::settings::USAGE_ITEM_SECTION_PREFIX)
{
continue;
}
let raw_id = id
Expand All @@ -213,6 +281,35 @@ pub(crate) fn usage_item_descriptors(
push(raw_id, "", false);
}

if provider_id == ProviderId::Claude {
let reported = snapshot.map(claude_reported_sections).unwrap_or_default();
let saved = hidden.iter().map(String::as_str).filter(|id| {
id.starts_with(codexbar::settings::USAGE_ITEM_SECTION_PREFIX) && !reported.contains(id)
});
let rows = reported
.iter()
.map(|id| (*id, true))
.chain(saved.map(|id| (id, false)))
.collect::<Vec<_>>();
for (id, available) in rows {
let Some((title, requires_cost_summary)) = claude_section_title(id, lang) else {
continue;
};
if seen.insert(id.to_string()) {
items.push(ProviderUsageItemSnapshot {
id: id.to_string(),
title: if available {
title
} else {
locale::format_locale(lang, LocaleKey::UsageItemUnavailableTitle, &[&title])
},
available,
requires_cost_summary,
});
}
}
}

items
}

Expand Down
Loading