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 @@ -13,6 +13,7 @@
- Menu: move each usage window's used percentage and reset time into its title row, with all pace detail on one line (#2182). Thanks @jack24254029!

### Fixed
- Command Code: parse and display 5-hour and weekly rolling limits alongside monthly credits and reset times (#2466). Thanks @derekszen!
- Usage & Spend: keep validated Codex totals visible while the local scanner catches up, with refresh indicators in the dashboard and menu cost rows (#2397). Thanks @hhh2210!
- ZoomMate: preserve browser cookie scope so parent-domain sessions reach both API hosts without leaking host-only cookies (fixes #2507). Thanks @weddle!
- Sync: propagate provider configuration edits made by the CLI or directly in `config.json` to the iCloud fleet without echoing remotely applied writes.
Expand Down
6 changes: 1 addition & 5 deletions Sources/CodexBar/UsageStore+PlanUtilization.swift
Original file line number Diff line number Diff line change
Expand Up @@ -475,11 +475,7 @@ extension UsageStore {
context: LimitResetDetectionContext,
samples: [PlanUtilizationSeriesSample])
{
let shouldIgnoreCommandCode = context.provider == .commandcode
&& context.snapshot.commandCodeSubscriptionEnrichmentUnavailable
let sessionObservation: LimitResetObservation? = if shouldIgnoreCommandCode {
nil
} else if context.provider == .codex {
let sessionObservation: LimitResetObservation? = if context.provider == .codex {
samples.last(where: { $0.name == .session }).map {
LimitResetObservation(
usedPercent: $0.entry.usedPercent,
Expand Down
1 change: 0 additions & 1 deletion Sources/CodexBar/UsageStore+QuotaWarnings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ extension UsageStore {
self.clearQuotaLowHookUsage(provider: provider)
}
guard notificationsEnabled || hooksActive else { return }
if provider == .commandcode, snapshot.commandCodeSubscriptionEnrichmentUnavailable { return }

let accountContext = QuotaWarningAccountContext(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve Command Code warning thresholds on partial refreshes

For the same Command Code enrichment-failure snapshots that lack windowLimits, continuing past this point leaves primaryWindow/secondaryWindow nil and handleQuotaWarningTransition removes the stored fired thresholds. A later successful rolling window that is still below the configured threshold is therefore reported as a fresh crossing and can re-send the same quota_low notification or hook; skip state mutation only for enrichment-unavailable refreshes without rolling windows instead of clearing the warning state.

Useful? React with 👍 / 👎.

discriminator: accountDiscriminator,
Expand Down
12 changes: 6 additions & 6 deletions Sources/CodexBar/UsageStore+Refresh.swift
Original file line number Diff line number Diff line change
Expand Up @@ -98,20 +98,20 @@ extension UsageStore {
let previousProvesPaidDepletion = previous?.commandCodeHasSubscriptionPlan == true ||
(previous?.commandCodeSubscriptionEnrichmentUnavailable == true &&
previous?.commandCodeMonthlyGrantDepleted == true &&
previous?.primary?.usedPercent == 100)
previous?.tertiary?.usedPercent == 100)
guard current.commandCodeSubscriptionEnrichmentUnavailable,
current.commandCodeMonthlyGrantDepleted,
previousProvesPaidDepletion,
let previousPrimary = previous?.primary
let previousMonthly = previous?.tertiary
else {
return current
}
let depleted = RateWindow(
usedPercent: 100,
windowMinutes: previousPrimary.windowMinutes,
resetsAt: previousPrimary.resetsAt,
resetDescription: previousPrimary.resetDescription)
return current.with(primary: depleted, secondary: current.secondary)
windowMinutes: previousMonthly.windowMinutes,
resetsAt: previousMonthly.resetsAt,
resetDescription: previousMonthly.resetDescription)
return current.with(tertiary: depleted)
}

func refreshForSettingsChange() async {
Expand Down
11 changes: 0 additions & 11 deletions Sources/CodexBar/UsageStore+SessionQuotaTransition.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,6 @@ extension UsageStore {
{
// Session quota notifications are tied to the primary session window. Copilot free plans can
// expose only chat quota, so allow Copilot to fall back to secondary for transition tracking.
// Command Code synthesizes a depleted primary while subscription enrichment is unavailable.
// Preserve the prior notification state for that placeholder, but accept positive credit data.
if provider == .commandcode,
snapshot.commandCodeSubscriptionEnrichmentUnavailable,
SessionQuotaNotificationLogic.isDepleted(snapshot.primary?.remainingPercent)
{
return
}
// Hooks have their own enable switch, so a configured quota_reached hook must fire on a
// real depletion even when session quota notifications are off. Run transition detection
// whenever notifications OR a matching hook rule is active; gate the OS notification post
Expand All @@ -37,9 +29,6 @@ extension UsageStore {
return
}
guard let sessionWindow = self.sessionQuotaWindow(provider: provider, snapshot: snapshot) else {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve Command Code session state on partial refreshes

When Command Code's optional subscription request times out or fails and the credits response does not include windowLimits (the existing failure-test credits payload still has that shape), sessionQuotaWindow is nil here and the non-Codex branch below clears sessionQuotaTransitionStates. If the user was already depleted, the next successful 5-hour snapshot is evaluated with no previous state and posts quota_reached/the depleted notification again, so enrichment failures can recreate duplicate depletion alerts; retain the prior Command Code state for enrichment-unavailable snapshots that carry no real rolling window.

Useful? React with 👍 / 👎.

if provider == .commandcode, snapshot.commandCodeSubscriptionEnrichmentUnavailable {
return
}
if provider == .codex {
if let previous = self.sessionQuotaTransitionStates[.codex] {
if previous.codexOwnerKey != codexOwnerKey {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ public enum CommandCodeProviderDescriptor {
metadata: ProviderMetadata(
id: .commandcode,
displayName: "Command Code",
sessionLabel: "Monthly credits",
weeklyLabel: "Monthly",
opusLabel: nil,
supportsOpus: false,
sessionLabel: "5-hour",
weeklyLabel: "Weekly",
opusLabel: "Monthly",
supportsOpus: true,
supportsCredits: true,
creditsHint: "Monthly USD credits from Command Code billing.",
creditsHint: "Monthly USD credits and rolling usage limits from Command Code billing.",
toggleTitle: "Show Command Code usage",
cliName: "commandcode",
defaultEnabled: false,
Expand All @@ -38,6 +38,7 @@ public enum CommandCodeProviderDescriptor {
tokenCost: ProviderTokenCostConfig(
supportsTokenCost: false,
noDataMessage: { "Command Code cost summary is not yet supported." }),
pace: .calendarMonthResetWindow,
fetchPlan: ProviderFetchPlan(
sourceModes: [.auto, .web],
pipeline: ProviderFetchPipeline(resolveStrategies: { _ in [CommandCodeWebFetchStrategy()] })),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ public enum CommandCodeUsageFetcher {
purchasedCredits: credits.purchasedCredits,
premiumMonthlyCredits: credits.premiumMonthlyCredits,
opensourceMonthlyCredits: credits.opensourceMonthlyCredits,
fiveHourWindow: credits.fiveHourWindow,
weeklyWindow: credits.weeklyWindow,
plan: plan,
billingPeriodEnd: subscription?.currentPeriodEnd,
subscriptionStatus: subscription?.status,
Expand Down Expand Up @@ -126,6 +128,8 @@ public enum CommandCodeUsageFetcher {
let purchasedCredits: Double
let premiumMonthlyCredits: Double
let opensourceMonthlyCredits: Double
let fiveHourWindow: RateWindow?
let weeklyWindow: RateWindow?
}

struct SubscriptionPayload {
Expand Down Expand Up @@ -199,11 +203,19 @@ public enum CommandCodeUsageFetcher {
guard let monthly = self.double(from: credits["monthlyCredits"]) else {
throw CommandCodeUsageError.parseFailed("Credits: missing monthlyCredits")
}
let windowLimits = (root["windowLimits"] as? [String: Any])
?? (credits["windowLimits"] as? [String: Any])
return CreditsPayload(
monthlyCredits: monthly,
purchasedCredits: self.double(from: credits["purchasedCredits"]) ?? 0,
premiumMonthlyCredits: self.double(from: credits["premiumMonthlyCredits"]) ?? 0,
opensourceMonthlyCredits: self.double(from: credits["opensourceMonthlyCredits"]) ?? 0)
opensourceMonthlyCredits: self.double(from: credits["opensourceMonthlyCredits"]) ?? 0,
fiveHourWindow: self.rateWindow(
from: windowLimits?["fiveHour"],
windowMinutes: 5 * 60),
weeklyWindow: self.rateWindow(
from: windowLimits?["weekly"],
windowMinutes: 7 * 24 * 60))
}

static func parseSubscription(data: Data) throws -> SubscriptionPayload? {
Expand Down Expand Up @@ -234,6 +246,21 @@ public enum CommandCodeUsageFetcher {
return SubscriptionPayload(planID: planID, status: status, currentPeriodEnd: periodEnd)
}

private static func rateWindow(from value: Any?, windowMinutes: Int) -> RateWindow? {
guard let limit = value as? [String: Any],
let cap = self.double(from: limit["cap"]),
cap > 0
else {
return nil
}
let used = self.double(from: limit["used"]) ?? 0
return RateWindow(
usedPercent: UsagePercent(used: used, limit: cap).displayClamped,
windowMinutes: windowMinutes,
resetsAt: self.date(from: limit["resetAt"]),
resetDescription: nil)
}

// MARK: - Value coercion

private static func double(from value: Any?) -> Double? {
Expand All @@ -250,12 +277,18 @@ public enum CommandCodeUsageFetcher {
}

private static func date(from value: Any?) -> Date? {
if let timestamp = self.double(from: value), timestamp > 0 {
let seconds = timestamp > 10_000_000_000 ? timestamp / 1000 : timestamp
return Date(timeIntervalSince1970: seconds)
}
guard let s = value as? String else { return nil }
let trimmed = s.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return nil }
let fractional = ISO8601DateFormatter()
fractional.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
if let date = fractional.date(from: trimmed) { return date }
if let date = fractional.date(from: trimmed) {
return date
}
let plain = ISO8601DateFormatter()
plain.formatOptions = [.withInternetDateTime]
return plain.date(from: trimmed)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Foundation

/// Parsed view of CommandCode `/internal/billing/credits` + `/internal/billing/subscriptions`.
/// Parsed view of Command Code billing credits, rolling limits, and subscription state.
public struct CommandCodeUsageSnapshot: Sendable {
/// USD remaining in the current monthly grant (`credits.monthlyCredits`).
public let monthlyCreditsRemaining: Double
Expand All @@ -10,6 +10,10 @@ public struct CommandCodeUsageSnapshot: Sendable {
public let premiumMonthlyCredits: Double
/// USD remaining in the open-source monthly grant (`credits.opensourceMonthlyCredits`).
public let opensourceMonthlyCredits: Double
/// Rolling five-hour usage limit reported by the credits response.
public let fiveHourWindow: RateWindow?
/// Rolling weekly usage limit reported by the credits response.
public let weeklyWindow: RateWindow?
/// Subscription plan, or nil when the user is on the free tier.
public let plan: CommandCodePlanCatalog.Plan?
/// `currentPeriodEnd` from the active subscription.
Expand All @@ -25,6 +29,8 @@ public struct CommandCodeUsageSnapshot: Sendable {
purchasedCredits: Double,
premiumMonthlyCredits: Double,
opensourceMonthlyCredits: Double,
fiveHourWindow: RateWindow? = nil,
weeklyWindow: RateWindow? = nil,
plan: CommandCodePlanCatalog.Plan?,
billingPeriodEnd: Date?,
subscriptionStatus: String?,
Expand All @@ -35,6 +41,8 @@ public struct CommandCodeUsageSnapshot: Sendable {
self.purchasedCredits = purchasedCredits
self.premiumMonthlyCredits = premiumMonthlyCredits
self.opensourceMonthlyCredits = opensourceMonthlyCredits
self.fiveHourWindow = fiveHourWindow
self.weeklyWindow = weeklyWindow
self.plan = plan
self.billingPeriodEnd = billingPeriodEnd
self.subscriptionStatus = subscriptionStatus
Expand All @@ -54,7 +62,7 @@ public struct CommandCodeUsageSnapshot: Sendable {
}

public func toUsageSnapshot() -> UsageSnapshot {
let primary = self.makePrimaryWindow()
let monthly = self.makeMonthlyWindow()

let identity = ProviderIdentitySnapshot(
providerID: .commandcode,
Expand All @@ -63,9 +71,9 @@ public struct CommandCodeUsageSnapshot: Sendable {
loginMethod: self.makeLoginMethod())

return UsageSnapshot(
primary: primary,
secondary: nil,
tertiary: nil,
primary: self.fiveHourWindow,
secondary: self.weeklyWindow,
tertiary: monthly,
providerCost: nil,
commandCodeSubscriptionEnrichmentUnavailable: self.subscriptionEnrichmentUnavailable,
commandCodeHasSubscriptionPlan: self.plan != nil,
Expand All @@ -74,13 +82,13 @@ public struct CommandCodeUsageSnapshot: Sendable {
identity: identity)
}

private func makePrimaryWindow() -> RateWindow? {
private func makeMonthlyWindow() -> RateWindow? {
guard let total = self.monthlyCreditsTotal, total > 0 else {
// Free / unknown plan with no allowance — surface 100% so the bar renders empty.
if self.monthlyCreditsRemaining > 0 || self.purchasedCredits > 0 {
return RateWindow(
usedPercent: 0,
windowMinutes: nil,
windowMinutes: ProviderPaceCapability.monthlyWindowSentinelMinutes,
resetsAt: self.billingPeriodEnd,
resetDescription: nil)
}
Expand All @@ -90,7 +98,7 @@ public struct CommandCodeUsageSnapshot: Sendable {
let percent = UsagePercent(used: used, limit: total).displayClamped
return RateWindow(
usedPercent: percent,
windowMinutes: nil,
windowMinutes: ProviderPaceCapability.monthlyWindowSentinelMinutes,
resetsAt: self.billingPeriodEnd,
resetDescription: nil)
}
Expand Down
4 changes: 4 additions & 0 deletions Sources/CodexBarCore/UsageFetcher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,10 @@ public struct UsageSnapshot: Codable, Sendable {
secondary: .value(secondary))
}

public func with(tertiary: RateWindow?) -> UsageSnapshot {
self.replacing(tertiary: .value(tertiary))
}

public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.primary = try container.decodeIfPresent(RateWindow.self, forKey: .primary)
Expand Down
5 changes: 5 additions & 0 deletions Tests/CodexBarTests/CommandCodeProviderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ struct CommandCodeProviderTests {
#expect(descriptor.metadata.cliName == "commandcode")
#expect(descriptor.branding.iconResourceName == "ProviderIcon-commandcode")
#expect(descriptor.branding.iconStyle == .commandcode)
#expect(descriptor.metadata.sessionLabel == "5-hour")
#expect(descriptor.metadata.weeklyLabel == "Weekly")
#expect(descriptor.metadata.opusLabel == "Monthly")
#expect(descriptor.metadata.supportsOpus)
#expect(descriptor.fetchPlan.sourceModes == [.auto, .web])
}

@Test
Expand Down
Loading