Skip to content
Open
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
25 changes: 25 additions & 0 deletions CopilotMonitor/CopilotMonitor/App/StatusBarController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -915,6 +915,24 @@ final class StatusBarController: NSObject {
return details.dailyUsage
}

private func chutesMonthlyPercentFromDetails(_ details: DetailedUsage?) -> Double? {
guard let details else { return nil }

let configuredPlan = SubscriptionSettingsManager.shared.getPlan(for: .chutes)
let configuredCapUSD = configuredPlan.isSet
? configuredPlan.cost * ChutesProvider.monthlyValueMultiplier
: nil
let capUSD = configuredCapUSD ?? details.chutesMonthlyValueCapUSD

if let usedUSD = details.chutesMonthlyValueUsedUSD,
let capUSD,
capUSD > 0 {
return min(max((usedUSD / capUSD) * 100.0, 0), 999)
}

return details.chutesMonthlyValueUsedPercent
}

private func usagePercentCandidates(
identifier: ProviderIdentifier,
usage: ProviderUsage,
Expand Down Expand Up @@ -954,6 +972,7 @@ final class StatusBarController: NSObject {
case .nanoGpt:
add(details?.sevenDayUsage, priority: .weekly)
case .chutes:
add(chutesMonthlyPercentFromDetails(details), priority: .monthly)
add(dailyPercentFromDetails(details), priority: .daily)
case .synthetic:
add(details?.fiveHourUsage, priority: .hourly)
Expand Down Expand Up @@ -1910,6 +1929,9 @@ final class StatusBarController: NSObject {
} else if identifier == .zaiCodingPlan {
let percents = [account.details?.tokenUsagePercent, account.details?.mcpUsagePercent].compactMap { $0 }
usedPercents = percents.isEmpty ? [account.usage.usagePercentage] : percents
} else if identifier == .chutes {
let percents = [dailyPercentFromDetails(account.details), chutesMonthlyPercentFromDetails(account.details)].compactMap { $0 }
usedPercents = percents.isEmpty ? [account.usage.usagePercentage] : percents
} else if identifier == .nanoGpt {
let percents = [
account.details?.sevenDayUsage,
Expand Down Expand Up @@ -1963,6 +1985,9 @@ final class StatusBarController: NSObject {
} else if identifier == .zaiCodingPlan {
let percents = [result.details?.tokenUsagePercent, result.details?.mcpUsagePercent].compactMap { $0 }
usedPercents = percents.isEmpty ? [singlePercent] : percents
} else if identifier == .chutes {
let percents = [dailyPercentFromDetails(result.details), chutesMonthlyPercentFromDetails(result.details)].compactMap { $0 }
usedPercents = percents.isEmpty ? [singlePercent] : percents
} else if identifier == .nanoGpt {
let percents = [
result.details?.sevenDayUsage,
Expand Down
48 changes: 43 additions & 5 deletions CopilotMonitor/CopilotMonitor/Helpers/ProviderMenuBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -573,31 +573,51 @@ extension StatusBarController {
addSubscriptionItems(to: submenu, provider: .nanoGpt, accountId: accountId)

case .chutes:
if let plan = details.planType {
let item = NSMenuItem()
item.view = createDisabledLabelView(text: "Plan: \(plan)")
submenu.addItem(item)
}

if let daily = details.dailyUsage,
let limit = details.limit {
let used = Int(daily)
let total = Int(limit)
let percentage = total > 0 ? Int((Double(used) / Double(total)) * 100) : 0

let item = NSMenuItem()
item.view = createDisabledLabelView(text: String(format: "Daily: %d%% used (%d/%d)", percentage, used, total))
item.view = createDisabledLabelView(text: String(format: "Daily Requests: %d / %d (%.0f%% used)", used, total, Double(percentage)))
submenu.addItem(item)
}

submenu.addItem(NSMenuItem.separator())
let chutesMonthlyValue = resolvedChutesMonthlyValuePresentation(details: details)

if let plan = details.planType {
if let usedUSD = chutesMonthlyValue.usedUSD,
let capUSD = chutesMonthlyValue.capUSD,
let usedPercent = chutesMonthlyValue.usedPercent {
let item = NSMenuItem()
item.view = createDisabledLabelView(text: "Plan: \(plan)")
item.view = createDisabledLabelView(
text: String(format: "Monthly Value Used: $%.2f / $%.2f (%.0f%% used)", usedUSD, capUSD, usedPercent)
)
submenu.addItem(item)
} else if let capUSD = chutesMonthlyValue.capUSD {
let item = NSMenuItem()
item.view = createDisabledLabelView(
text: String(format: "Monthly Cap: $%.2f (5× subscription)", capUSD)
)
submenu.addItem(item)
}

if let credits = details.creditsBalance {
let item = NSMenuItem()
item.view = createDisabledLabelView(text: String(format: "Credits: $%.2f", credits))
item.view = createDisabledLabelView(text: String(format: "Credits Balance: $%.2f", credits))
submenu.addItem(item)
}

let overageItem = NSMenuItem()
overageItem.view = createDisabledLabelView(text: "Overage: PAYGO after cap")
submenu.addItem(overageItem)

addSubscriptionItems(to: submenu, provider: .chutes)

case .synthetic:
Expand Down Expand Up @@ -721,6 +741,24 @@ extension StatusBarController {
}?.email
}

private func resolvedChutesMonthlyValuePresentation(details: DetailedUsage) -> (usedUSD: Double?, capUSD: Double?, usedPercent: Double?) {
let configuredPlan = SubscriptionSettingsManager.shared.getPlan(for: .chutes)
let configuredCapUSD = configuredPlan.isSet
? configuredPlan.cost * ChutesProvider.monthlyValueMultiplier
: nil
let capUSD = configuredCapUSD ?? details.chutesMonthlyValueCapUSD
let usedUSD = details.chutesMonthlyValueUsedUSD

let usedPercent: Double?
if let usedUSD, let capUSD, capUSD > 0 {
usedPercent = min(max((usedUSD / capUSD) * 100.0, 0), 999)
} else {
usedPercent = details.chutesMonthlyValueUsedPercent
}

return (usedUSD, capUSD, usedPercent)
}

private func addGroupedModelUsageSection(
to submenu: NSMenu,
modelBreakdown: [String: Double],
Expand Down
22 changes: 21 additions & 1 deletion CopilotMonitor/CopilotMonitor/Models/ProviderResult.swift
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,11 @@ struct DetailedUsage {
let creditsBalance: Double?
let planType: String?

// Chutes-specific value cap tracking
let chutesMonthlyValueCapUSD: Double?
let chutesMonthlyValueUsedUSD: Double?
let chutesMonthlyValueUsedPercent: Double?

// Claude extra usage toggle
let extraUsageEnabled: Bool?
// Claude extra usage (monthly credits limit + usage)
Expand Down Expand Up @@ -184,6 +189,9 @@ struct DetailedUsage {
sparkWindowLabel: String? = nil,
creditsBalance: Double? = nil,
planType: String? = nil,
chutesMonthlyValueCapUSD: Double? = nil,
chutesMonthlyValueUsedUSD: Double? = nil,
chutesMonthlyValueUsedPercent: Double? = nil,
extraUsageEnabled: Bool? = nil,
extraUsageMonthlyLimitUSD: Double? = nil,
extraUsageUsedUSD: Double? = nil,
Expand Down Expand Up @@ -247,6 +255,9 @@ struct DetailedUsage {
self.sparkWindowLabel = sparkWindowLabel
self.creditsBalance = creditsBalance
self.planType = planType
self.chutesMonthlyValueCapUSD = chutesMonthlyValueCapUSD
self.chutesMonthlyValueUsedUSD = chutesMonthlyValueUsedUSD
self.chutesMonthlyValueUsedPercent = chutesMonthlyValueUsedPercent
self.extraUsageEnabled = extraUsageEnabled
self.extraUsageMonthlyLimitUSD = extraUsageMonthlyLimitUSD
self.extraUsageUsedUSD = extraUsageUsedUSD
Expand Down Expand Up @@ -292,7 +303,9 @@ extension DetailedUsage: Codable {
case sonnetUsage, sonnetReset, opusUsage, opusReset, modelBreakdown, modelResetTimes
case secondaryUsage, secondaryReset, primaryReset
case sparkUsage, sparkReset, sparkSecondaryUsage, sparkSecondaryReset, sparkWindowLabel
case creditsBalance, planType, extraUsageEnabled
case creditsBalance, planType
case chutesMonthlyValueCapUSD, chutesMonthlyValueUsedUSD, chutesMonthlyValueUsedPercent
case extraUsageEnabled
case extraUsageMonthlyLimitUSD, extraUsageUsedUSD, extraUsageUtilizationPercent
case sessions, messages, avgCostPerDay, email
case dailyHistory, monthlyCost, creditsRemaining, creditsTotal
Expand Down Expand Up @@ -334,6 +347,9 @@ extension DetailedUsage: Codable {
sparkWindowLabel = try container.decodeIfPresent(String.self, forKey: .sparkWindowLabel)
creditsBalance = try container.decodeIfPresent(Double.self, forKey: .creditsBalance)
planType = try container.decodeIfPresent(String.self, forKey: .planType)
chutesMonthlyValueCapUSD = try container.decodeIfPresent(Double.self, forKey: .chutesMonthlyValueCapUSD)
chutesMonthlyValueUsedUSD = try container.decodeIfPresent(Double.self, forKey: .chutesMonthlyValueUsedUSD)
chutesMonthlyValueUsedPercent = try container.decodeIfPresent(Double.self, forKey: .chutesMonthlyValueUsedPercent)
extraUsageEnabled = try container.decodeIfPresent(Bool.self, forKey: .extraUsageEnabled)
extraUsageMonthlyLimitUSD = try container.decodeIfPresent(Double.self, forKey: .extraUsageMonthlyLimitUSD)
extraUsageUsedUSD = try container.decodeIfPresent(Double.self, forKey: .extraUsageUsedUSD)
Expand Down Expand Up @@ -400,6 +416,9 @@ extension DetailedUsage: Codable {
try container.encodeIfPresent(sparkWindowLabel, forKey: .sparkWindowLabel)
try container.encodeIfPresent(creditsBalance, forKey: .creditsBalance)
try container.encodeIfPresent(planType, forKey: .planType)
try container.encodeIfPresent(chutesMonthlyValueCapUSD, forKey: .chutesMonthlyValueCapUSD)
try container.encodeIfPresent(chutesMonthlyValueUsedUSD, forKey: .chutesMonthlyValueUsedUSD)
try container.encodeIfPresent(chutesMonthlyValueUsedPercent, forKey: .chutesMonthlyValueUsedPercent)
try container.encodeIfPresent(extraUsageEnabled, forKey: .extraUsageEnabled)
try container.encodeIfPresent(extraUsageMonthlyLimitUSD, forKey: .extraUsageMonthlyLimitUSD)
try container.encodeIfPresent(extraUsageUsedUSD, forKey: .extraUsageUsedUSD)
Expand Down Expand Up @@ -784,6 +803,7 @@ extension DetailedUsage {
|| secondaryUsage != nil || secondaryReset != nil || primaryReset != nil
|| sparkUsage != nil || sparkReset != nil || sparkSecondaryUsage != nil || sparkSecondaryReset != nil || sparkWindowLabel != nil
|| creditsBalance != nil || planType != nil
|| chutesMonthlyValueCapUSD != nil || chutesMonthlyValueUsedUSD != nil || chutesMonthlyValueUsedPercent != nil
|| extraUsageEnabled != nil
|| extraUsageMonthlyLimitUSD != nil || extraUsageUsedUSD != nil || extraUsageUtilizationPercent != nil
|| sessions != nil || messages != nil || avgCostPerDay != nil
Expand Down
Loading