Skip to content
Draft
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
7 changes: 5 additions & 2 deletions ClaudeUsage/ClaudeUsageApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,12 @@ class AppDelegate: NSObject, NSApplicationDelegate {
guard let button = statusItem?.button else { return }

if let usage = usageManager.usage {
let sessionPct = usage.sessionPercentage
let emoji = usageManager.statusEmoji
button.title = "\(emoji) \(sessionPct)%"
if usage.extraUsageEnabled, let used = usage.extraUsageUsedCredits {
button.title = "\(emoji) $\(String(format: "%.0f", used / 100))"
} else {
button.title = "\(emoji) \(usage.sessionPercentage)%"
}
} else if usageManager.error != nil {
button.title = "❌"
} else {
Expand Down
11 changes: 8 additions & 3 deletions ClaudeUsage/UsageManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,14 @@ class UsageManager: ObservableObject {

var statusEmoji: String {
guard let usage = usage else { return "❓" }
let maxUtil = max(usage.sessionUtilization, usage.weeklyUtilization)
if maxUtil >= 90 { return "πŸ”΄" }
if maxUtil >= 70 { return "🟑" }
let util: Double
if usage.extraUsageEnabled, let pct = usage.extraUsagePercentage {
util = Double(pct)
} else {
util = max(usage.sessionUtilization, usage.weeklyUtilization)
}
if util >= 90 { return "πŸ”΄" }
if util >= 70 { return "🟑" }
return "🟒"
}

Expand Down
58 changes: 28 additions & 30 deletions ClaudeUsage/UsageView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -71,42 +71,40 @@ struct UsageView: View {
@ViewBuilder
func usageContent(_ usage: UsageData) -> some View {
VStack(spacing: 16) {
// Session usage
UsageRow(
title: "Session",
subtitle: "5-hour window",
percentage: usage.sessionPercentage,
resetsAt: usage.sessionResetsAt,
color: colorForPercentage(usage.sessionPercentage)
)

// Weekly usage
UsageRow(
title: "Weekly",
subtitle: "7-day window",
percentage: usage.weeklyPercentage,
resetsAt: usage.weeklyResetsAt,
color: colorForPercentage(usage.weeklyPercentage)
)

// Sonnet only (if available)
if let sonnetPct = usage.sonnetPercentage {
UsageRow(
title: "Sonnet Only",
subtitle: "Model-specific",
percentage: sonnetPct,
resetsAt: usage.sonnetResetsAt,
color: colorForPercentage(sonnetPct)
)
}

// Extra usage / overage (if enabled)
if usage.extraUsageEnabled, let limit = usage.extraUsageMonthlyLimit, let used = usage.extraUsageUsedCredits {
// Enterprise: spend row only
OverageRow(
usedDollars: used / 100,
limitDollars: limit / 100,
percentage: usage.extraUsagePercentage ?? 0
)
} else {
// Non-enterprise: rate-limit rows
UsageRow(
title: "Session",
subtitle: "5-hour window",
percentage: usage.sessionPercentage,
resetsAt: usage.sessionResetsAt,
color: colorForPercentage(usage.sessionPercentage)
)

UsageRow(
title: "Weekly",
subtitle: "7-day window",
percentage: usage.weeklyPercentage,
resetsAt: usage.weeklyResetsAt,
color: colorForPercentage(usage.weeklyPercentage)
)

if let sonnetPct = usage.sonnetPercentage {
UsageRow(
title: "Sonnet Only",
subtitle: "Model-specific",
percentage: sonnetPct,
resetsAt: usage.sonnetResetsAt,
color: colorForPercentage(sonnetPct)
)
}
}
}
.padding()
Expand Down
184 changes: 184 additions & 0 deletions ClaudeUsageTests/EnterpriseDisplayTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import XCTest

// These tests mirror the logic in UsageData and UsageManager without importing
// the app module (which is an executable target and cannot be @testable imported).
// They exist to pin the expected behaviour of the enterprise display changes.

// MARK: - Minimal replicas of app types

private struct UsageData {
let sessionUtilization: Double
let weeklyUtilization: Double
let extraUsageEnabled: Bool
let extraUsageMonthlyLimit: Double?
let extraUsageUsedCredits: Double?

var sessionPercentage: Int { Int(sessionUtilization) }

var extraUsagePercentage: Int? {
guard extraUsageEnabled,
let limit = extraUsageMonthlyLimit,
let used = extraUsageUsedCredits,
limit > 0 else { return nil }
return Int((used / limit) * 100)
}
}

private func statusEmoji(for usage: UsageData) -> String {
let util: Double
if usage.extraUsageEnabled, let pct = usage.extraUsagePercentage {
util = Double(pct)
} else {
util = max(usage.sessionUtilization, usage.weeklyUtilization)
}
if util >= 90 { return "πŸ”΄" }
if util >= 70 { return "🟑" }
return "🟒"
}

private func menubarLabel(for usage: UsageData, emoji: String) -> String {
if usage.extraUsageEnabled, let used = usage.extraUsageUsedCredits {
return "\(emoji) $\(String(format: "%.0f", used / 100))"
}
return "\(emoji) \(usage.sessionPercentage)%"
}

// MARK: - Tests

final class EnterpriseDisplayTests: XCTestCase {

// MARK: extraUsagePercentage

func testExtraUsagePercentage_typicalSpend_calculatesCorrectly() {
// $14.03 spent of $100 limit (stored as cents)
let usage = UsageData(sessionUtilization: 0, weeklyUtilization: 0,
extraUsageEnabled: true,
extraUsageMonthlyLimit: 10_000,
extraUsageUsedCredits: 1_403)
XCTAssertEqual(usage.extraUsagePercentage, 14)
}

func testExtraUsagePercentage_fullLimit_returns100() {
let usage = UsageData(sessionUtilization: 0, weeklyUtilization: 0,
extraUsageEnabled: true,
extraUsageMonthlyLimit: 10_000,
extraUsageUsedCredits: 10_000)
XCTAssertEqual(usage.extraUsagePercentage, 100)
}

func testExtraUsagePercentage_noSpend_returnsZero() {
let usage = UsageData(sessionUtilization: 0, weeklyUtilization: 0,
extraUsageEnabled: true,
extraUsageMonthlyLimit: 10_000,
extraUsageUsedCredits: 0)
XCTAssertEqual(usage.extraUsagePercentage, 0)
}

func testExtraUsagePercentage_whenDisabled_returnsNil() {
let usage = UsageData(sessionUtilization: 50, weeklyUtilization: 50,
extraUsageEnabled: false,
extraUsageMonthlyLimit: 10_000,
extraUsageUsedCredits: 1_403)
XCTAssertNil(usage.extraUsagePercentage)
}

func testExtraUsagePercentage_zeroLimit_returnsNil() {
let usage = UsageData(sessionUtilization: 0, weeklyUtilization: 0,
extraUsageEnabled: true,
extraUsageMonthlyLimit: 0,
extraUsageUsedCredits: 1_403)
XCTAssertNil(usage.extraUsagePercentage)
}

func testExtraUsagePercentage_missingLimit_returnsNil() {
let usage = UsageData(sessionUtilization: 0, weeklyUtilization: 0,
extraUsageEnabled: true,
extraUsageMonthlyLimit: nil,
extraUsageUsedCredits: 1_403)
XCTAssertNil(usage.extraUsagePercentage)
}

// MARK: statusEmoji β€” enterprise path

func testStatusEmoji_enterprise_lowUsage_isGreen() {
// 14% of limit
let usage = UsageData(sessionUtilization: 0, weeklyUtilization: 0,
extraUsageEnabled: true,
extraUsageMonthlyLimit: 10_000,
extraUsageUsedCredits: 1_403)
XCTAssertEqual(statusEmoji(for: usage), "🟒")
}

func testStatusEmoji_enterprise_mediumUsage_isYellow() {
// 75% of limit
let usage = UsageData(sessionUtilization: 0, weeklyUtilization: 0,
extraUsageEnabled: true,
extraUsageMonthlyLimit: 10_000,
extraUsageUsedCredits: 7_500)
XCTAssertEqual(statusEmoji(for: usage), "🟑")
}

func testStatusEmoji_enterprise_highUsage_isRed() {
// 95% of limit
let usage = UsageData(sessionUtilization: 0, weeklyUtilization: 0,
extraUsageEnabled: true,
extraUsageMonthlyLimit: 10_000,
extraUsageUsedCredits: 9_500)
XCTAssertEqual(statusEmoji(for: usage), "πŸ”΄")
}

func testStatusEmoji_enterprise_ignoresSessionWeeklyUtilization() {
// Session/weekly are maxed out, but overage spend is only 1% β€” should be green
let usage = UsageData(sessionUtilization: 95, weeklyUtilization: 95,
extraUsageEnabled: true,
extraUsageMonthlyLimit: 10_000,
extraUsageUsedCredits: 100)
XCTAssertEqual(statusEmoji(for: usage), "🟒")
}

// MARK: statusEmoji β€” non-enterprise path

func testStatusEmoji_nonEnterprise_usesSessionWeeklyMax() {
let usage = UsageData(sessionUtilization: 85, weeklyUtilization: 60,
extraUsageEnabled: false,
extraUsageMonthlyLimit: nil,
extraUsageUsedCredits: nil)
XCTAssertEqual(statusEmoji(for: usage), "🟑") // max is 85%
}

func testStatusEmoji_nonEnterprise_highUsage_isRed() {
let usage = UsageData(sessionUtilization: 92, weeklyUtilization: 50,
extraUsageEnabled: false,
extraUsageMonthlyLimit: nil,
extraUsageUsedCredits: nil)
XCTAssertEqual(statusEmoji(for: usage), "πŸ”΄")
}

// MARK: Menubar label

func testMenubarLabel_enterprise_showsDollarAmount() {
// $14.03 β†’ rounded to $14 in menubar
let usage = UsageData(sessionUtilization: 0, weeklyUtilization: 0,
extraUsageEnabled: true,
extraUsageMonthlyLimit: 10_000,
extraUsageUsedCredits: 1_403)
XCTAssertEqual(menubarLabel(for: usage, emoji: "🟒"), "🟒 $14")
}

func testMenubarLabel_enterprise_roundsToNearestDollar() {
// $14.99 β†’ $15
let usage = UsageData(sessionUtilization: 0, weeklyUtilization: 0,
extraUsageEnabled: true,
extraUsageMonthlyLimit: 10_000,
extraUsageUsedCredits: 1_499)
XCTAssertEqual(menubarLabel(for: usage, emoji: "🟑"), "🟑 $15")
}

func testMenubarLabel_nonEnterprise_showsSessionPercentage() {
let usage = UsageData(sessionUtilization: 45, weeklyUtilization: 30,
extraUsageEnabled: false,
extraUsageMonthlyLimit: nil,
extraUsageUsedCredits: nil)
XCTAssertEqual(menubarLabel(for: usage, emoji: "🟒"), "🟒 45%")
}
}
5 changes: 5 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ let package = Package(
name: "ClaudeUsage",
dependencies: [],
path: "ClaudeUsage"
),
.testTarget(
name: "ClaudeUsageTests",
dependencies: [],
path: "ClaudeUsageTests"
)
]
)