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
7 changes: 5 additions & 2 deletions CopilotMonitor/CLI/CLIProviderManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ actor CLIProviderManager {
.antigravity, .openCodeZen, .openCodeGo, .kiro, .grok, .kimi, .minimaxCodingPlan, .zaiCodingPlan,
.nanoGpt,
.chutes, .copilot,
.synthetic
.synthetic,
.deepSeek

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Minor: Style Two provider registries: CLI and app can diverge
DeepSeek is now added to a hand-maintained identifier list here and separately to ProviderManager's default provider instances. Future provider additions can update one list and make CLI discovery disagree with actual fetching. Define registration once and derive identifiers from those instances, or make any intentional CLI-only filtering explicit.

]

// MARK: - Initialization
Expand All @@ -42,6 +43,7 @@ actor CLIProviderManager {
let nanoGptProvider = NanoGptProvider()
let chutesProvider = ChutesProvider()
let syntheticProvider = SyntheticProvider()
let deepSeekProvider = DeepSeekProvider()

// 1 CLI-specific provider (uses browser cookies instead of WebView)
let copilotCLIProvider = CopilotCLIProvider()
Expand All @@ -64,7 +66,8 @@ actor CLIProviderManager {
nanoGptProvider,
chutesProvider,
copilotCLIProvider,
syntheticProvider
syntheticProvider,
deepSeekProvider
]

let providerCount = providers.count
Expand Down
10 changes: 10 additions & 0 deletions CopilotMonitor/CopilotMonitor.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
A33333333333333333333333 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A44444444444444444444444 /* AppDelegate.swift */; };
A454D8C22F30544900E355E3 /* MenuBarExtraAccess in Frameworks */ = {isa = PBXBuildFile; productRef = MBA2222222222222222222222 /* MenuBarExtraAccess */; };
A454D8C42F30548900E355E3 /* ZaiCodingPlanProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A454D8C32F30548900E355E3 /* ZaiCodingPlanProvider.swift */; };
DE5E1B4A2C3D4E5F6A7B8C9DF /* DeepSeekProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE5E1B4A2C3D4E5F6A7B8C9DE /* DeepSeekProvider.swift */; };
MINIMAXAPP11111111111111 /* MiniMaxProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = MINIMAXFILE2222222222222 /* MiniMaxProvider.swift */; };
OCGOAPP11111111111111 /* OpenCodeGoProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = OCGOFILE2222222222222 /* OpenCodeGoProvider.swift */; };
GROKAPP11111111111111 /* GrokProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = GROKFILE2222222222222 /* GrokProvider.swift */; };
Expand Down Expand Up @@ -66,6 +67,7 @@
GROKCLI11111111111111 /* GrokProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = GROKFILE2222222222222 /* GrokProvider.swift */; };
KIROCLI11111111111111 /* KiroProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = KIROFILE2222222222222 /* KiroProvider.swift */; };
CLIZAI11111111111111111 /* ZaiCodingPlanProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A454D8C32F30548900E355E3 /* ZaiCodingPlanProvider.swift */; };
CLIDEEPSEEK11111111111 /* DeepSeekProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE5E1B4A2C3D4E5F6A7B8C9DE /* DeepSeekProvider.swift */; };
NANOGPTCLI11111111111111 /* NanoGptProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = NANOGPTFILE222222222222 /* NanoGptProvider.swift */; };
CLIOPENCZEN1111111111111 /* OpenCodeZenProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = OZ2222222222222222222222 /* OpenCodeZenProvider.swift */; };
CLIOPENROUTER111111111 /* OpenRouterProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = OR2222222222222222222222 /* OpenRouterProvider.swift */; };
Expand All @@ -90,6 +92,7 @@
SYNTHETIC1111111111111111 /* SyntheticProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = SYNTHETIC2222222222222222 /* SyntheticProvider.swift */; };
SYNTHTEST2222222222222222 /* SyntheticProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = SYNTHTEST1111111111111111 /* SyntheticProviderTests.swift */; };
NANOGPTTESTBF1111111111 /* NanoGptProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = NANOGPTTESTFR1111111111 /* NanoGptProviderTests.swift */; };
DEEPSKIPTESTBF1111111 /* DeepSeekProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEEPSKIPTESTFR1111111 /* DeepSeekProviderTests.swift */; };
TOKENTESTBF1111111111111 /* TokenManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = TOKENTESTFR1111111111111 /* TokenManagerTests.swift */; };
CODEXTESTBF111111111111 /* CodexProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CODEXTESTFR111111111111 /* CodexProviderTests.swift */; };
OCAUTHTESTBF11111111111 /* OpenCodeAuthDecodingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OCAUTHTESTFR11111111111 /* OpenCodeAuthDecodingTests.swift */; };
Expand Down Expand Up @@ -175,6 +178,7 @@
9B1085A77EF4A58E5B5EC71B /* CopilotHistoryService.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CopilotHistoryService.swift; sourceTree = "<group>"; };
A44444444444444444444444 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
A454D8C32F30548900E355E3 /* ZaiCodingPlanProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZaiCodingPlanProvider.swift; sourceTree = "<group>"; };
DE5E1B4A2C3D4E5F6A7B8C9DE /* DeepSeekProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeepSeekProvider.swift; sourceTree = "<group>"; };
MINIMAXFILE2222222222222 /* MiniMaxProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MiniMaxProvider.swift; sourceTree = "<group>"; };
OCGOFILE2222222222222 /* OpenCodeGoProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenCodeGoProvider.swift; sourceTree = "<group>"; };
GROKFILE2222222222222 /* GrokProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GrokProvider.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -233,6 +237,7 @@
SYNTHETIC2222222222222222 /* SyntheticProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyntheticProvider.swift; sourceTree = "<group>"; };
SYNTHTEST1111111111111111 /* SyntheticProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyntheticProviderTests.swift; sourceTree = "<group>"; };
NANOGPTTESTFR1111111111 /* NanoGptProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NanoGptProviderTests.swift; sourceTree = "<group>"; };
DEEPSKIPTESTFR1111111 /* DeepSeekProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeepSeekProviderTests.swift; sourceTree = "<group>"; };
TOKENTESTFR1111111111111 /* TokenManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenManagerTests.swift; sourceTree = "<group>"; };
CODEXTESTFR111111111111 /* CodexProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodexProviderTests.swift; sourceTree = "<group>"; };
OCAUTHTESTFR11111111111 /* OpenCodeAuthDecodingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenCodeAuthDecodingTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -289,6 +294,7 @@
isa = PBXGroup;
children = (
A454D8C32F30548900E355E3 /* ZaiCodingPlanProvider.swift */,
DE5E1B4A2C3D4E5F6A7B8C9DE /* DeepSeekProvider.swift */,
NANOGPTFILE222222222222 /* NanoGptProvider.swift */,
06EC3B683EB6892E4F9C8316 /* GeminiCLIProvider.swift */,
4DDC1B4DE6B5118CE4AE8F82 /* ClaudeProvider.swift */,
Expand Down Expand Up @@ -455,6 +461,7 @@
54353FD130DDE0500F6B367F /* MenuResultBuilderTests.swift */,
SYNTHTEST1111111111111111 /* SyntheticProviderTests.swift */,
NANOGPTTESTFR1111111111 /* NanoGptProviderTests.swift */,
DEEPSKIPTESTFR1111111 /* DeepSeekProviderTests.swift */,
MINIMAXTESTFR11111111111 /* MiniMaxProviderTests.swift */,
OCGOTESTFR11111111111 /* OpenCodeGoProviderTests.swift */,
GROKTESTFR11111111111 /* GrokProviderTests.swift */,
Expand Down Expand Up @@ -638,6 +645,7 @@
GROKCLI11111111111111 /* GrokProvider.swift in Sources */,
KIROCLI11111111111111 /* KiroProvider.swift in Sources */,
CLIZAI11111111111111111 /* ZaiCodingPlanProvider.swift in Sources */,
CLIDEEPSEEK11111111111 /* DeepSeekProvider.swift in Sources */,
NANOGPTCLI11111111111111 /* NanoGptProvider.swift in Sources */,
283349022F313176004DADE1 /* ChutesProvider.swift in Sources */,
CLITAVILY111111111111111 /* TavilySearchProvider.swift in Sources */,
Expand All @@ -658,6 +666,7 @@
A33333333333333333333333 /* AppDelegate.swift in Sources */,
AM1111111111111111111111 /* AppMigrationHelper.swift in Sources */,
A454D8C42F30548900E355E3 /* ZaiCodingPlanProvider.swift in Sources */,
DE5E1B4A2C3D4E5F6A7B8C9DF /* DeepSeekProvider.swift in Sources */,
NANOGPTAPP11111111111111 /* NanoGptProvider.swift in Sources */,
BCDE4599B74AF7A799CE1D /* StatusBarIconView.swift in Sources */,
ME1111111111111111111111 /* MenuEnums.swift in Sources */,
Expand Down Expand Up @@ -715,6 +724,7 @@
B58BAD3BFD97973070A2A892 /* MenuResultBuilderTests.swift in Sources */,
SYNTHTEST2222222222222222 /* SyntheticProviderTests.swift in Sources */,
NANOGPTTESTBF1111111111 /* NanoGptProviderTests.swift in Sources */,
DEEPSKIPTESTBF1111111 /* DeepSeekProviderTests.swift in Sources */,
MINIMAXTESTBF11111111111 /* MiniMaxProviderTests.swift in Sources */,
OCGOTESTBF11111111111 /* OpenCodeGoProviderTests.swift in Sources */,
GROKTESTBF11111111111 /* GrokProviderTests.swift in Sources */,
Expand Down
56 changes: 33 additions & 23 deletions CopilotMonitor/CopilotMonitor/App/StatusBarController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -917,20 +917,20 @@
return ProviderIdentifier.allCases.first(where: { isProviderEnabled($0) })
}

private func normalizedUsagePercent(_ percent: Double?) -> Double? {
private static func normalizedUsagePercent(_ percent: Double?) -> Double? {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Minor: Style Unneeded static conversion: maintenance churn
These four helpers were converted from instance methods to static, and the change rewrites a large set of callers to Self. without introducing a real type-level caller. That is pure churn on this feature path. Keep them as private instance helpers and restore the direct calls unless a concrete static use case exists.

guard let percent, percent.isFinite else { return nil }
return min(max(percent, 0), 999)
}

private func dailyPercentFromDetails(_ details: DetailedUsage?) -> Double? {
private static func dailyPercentFromDetails(_ details: DetailedUsage?) -> Double? {
guard let details else { return nil }
if let limit = details.limit, limit > 0, let used = details.dailyUsage {
return (used / limit) * 100.0
}
return details.dailyUsage
}

private func priorityForWindowHours(
private static func priorityForWindowHours(
_ hours: Int?,
fallback: UsageDisplayWindowPriority
) -> UsageDisplayWindowPriority {
Expand All @@ -941,7 +941,7 @@
return .hourly
}

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

let configuredPlan = SubscriptionSettingsManager.shared.getPlan(for: .chutes)
Expand All @@ -966,7 +966,7 @@
) -> [UsagePercentCandidate] {
var candidates: [UsagePercentCandidate] = []
func add(_ percent: Double?, priority: UsageDisplayWindowPriority) {
guard let normalized = normalizedUsagePercent(percent) else { return }
guard let normalized = Self.normalizedUsagePercent(percent) else { return }
candidates.append(UsagePercentCandidate(percent: normalized, priority: priority))
}

Expand Down Expand Up @@ -995,19 +995,19 @@
case .codex:
add(
details?.secondaryUsage,
priority: priorityForWindowHours(details?.codexSecondaryWindowHours, fallback: .weekly)
priority: Self.priorityForWindowHours(details?.codexSecondaryWindowHours, fallback: .weekly)
)
add(
details?.sparkSecondaryUsage,
priority: priorityForWindowHours(details?.sparkSecondaryWindowHours, fallback: .weekly)
priority: Self.priorityForWindowHours(details?.sparkSecondaryWindowHours, fallback: .weekly)
)
add(
dailyPercentFromDetails(details),
priority: priorityForWindowHours(details?.codexPrimaryWindowHours, fallback: .daily)
Self.dailyPercentFromDetails(details),
priority: Self.priorityForWindowHours(details?.codexPrimaryWindowHours, fallback: .daily)
)
add(
details?.sparkUsage,
priority: priorityForWindowHours(details?.sparkPrimaryWindowHours, fallback: .hourly)
priority: Self.priorityForWindowHours(details?.sparkPrimaryWindowHours, fallback: .hourly)
)
case .commandCode:
add(usage.usagePercentage, priority: .monthly)
Expand All @@ -1027,13 +1027,13 @@
case .nanoGpt:
add(details?.sevenDayUsage, priority: .weekly)
case .chutes:
add(chutesMonthlyPercentFromDetails(details), priority: .monthly)
add(dailyPercentFromDetails(details), priority: .daily)
add(Self.chutesMonthlyPercentFromDetails(details), priority: .monthly)
add(Self.dailyPercentFromDetails(details), priority: .daily)
case .synthetic:
add(details?.fiveHourUsage, priority: .hourly)
case .tavilySearch, .braveSearch:
add(details?.mcpUsagePercent, priority: .monthly)
case .antigravity, .geminiCLI, .openRouter, .openCode, .openCodeZen:
case .antigravity, .geminiCLI, .openRouter, .openCode, .openCodeZen, .deepSeek:
break
}

Expand Down Expand Up @@ -1085,7 +1085,7 @@
// Gemini CLI special case: add as fallback priority since these don't have window metadata
if identifier == .geminiCLI, let geminiAccounts = result.details?.geminiAccounts {
for account in geminiAccounts {
if let normalized = normalizedUsagePercent(100.0 - account.remainingPercentage) {
if let normalized = Self.normalizedUsagePercent(100.0 - account.remainingPercentage) {
allCandidates.append(UsagePercentCandidate(percent: normalized, priority: .fallback))
}
}
Expand All @@ -1108,7 +1108,7 @@

func appendMetrics(usage: ProviderUsage, details: DetailedUsage?) {
guard case .quotaBased = usage else { return }
if let percent = normalizedUsagePercent(usage.usagePercentage) {
if let percent = Self.normalizedUsagePercent(usage.usagePercentage) {
usedPercents.append(percent)
}

Expand All @@ -1129,7 +1129,7 @@
details.openCodeGoMonthlyUsage
]
for percent in extraPercents {
if let normalized = normalizedUsagePercent(percent) {
if let normalized = Self.normalizedUsagePercent(percent) {
usedPercents.append(normalized)
}
}
Expand All @@ -1146,7 +1146,7 @@

if identifier == .geminiCLI, let geminiAccounts = result.details?.geminiAccounts {
for account in geminiAccounts {
if let percent = normalizedUsagePercent(100.0 - account.remainingPercentage) {
if let percent = Self.normalizedUsagePercent(100.0 - account.remainingPercentage) {
usedPercents.append(percent)
}
}
Expand Down Expand Up @@ -1659,7 +1659,7 @@

var hasPayAsYouGo = false

let payAsYouGoOrder: [ProviderIdentifier] = [.openRouter, .openCodeZen]
let payAsYouGoOrder: [ProviderIdentifier] = [.openRouter, .openCodeZen, .deepSeek]
for identifier in payAsYouGoOrder {
guard isProviderEnabled(identifier) else { continue }

Expand All @@ -1677,9 +1677,17 @@
} else if let result {
if case .payAsYouGo(_, let cost, _) = result.usage {
hasPayAsYouGo = true
let costValue = cost ?? 0.0
// Balance-style providers (DeepSeek) leave `cost` nil and
// surface the remaining balance through details.
let costValue = cost ?? result.details?.creditsBalance ?? 0.0
let title: String
if let symbol = result.details?.balanceCurrencySymbol, !symbol.isEmpty {
title = String(format: "%@ (%@%.2f)", identifier.displayName, symbol, costValue)
} else {
title = String(format: "%@ ($%.2f)", identifier.displayName, costValue)
}
let item = NSMenuItem(
title: String(format: "%@ ($%.2f)", identifier.displayName, costValue),
title: title,
action: nil, keyEquivalent: ""
)
item.image = iconForProvider(identifier)
Expand Down Expand Up @@ -2139,7 +2147,7 @@
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 }
let percents = [Self.dailyPercentFromDetails(account.details), Self.chutesMonthlyPercentFromDetails(account.details)].compactMap { $0 }
usedPercents = percents.isEmpty ? [account.usage.usagePercentage] : percents
} else if identifier == .nanoGpt {
let percents = [
Expand Down Expand Up @@ -2225,7 +2233,7 @@
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 }
let percents = [Self.dailyPercentFromDetails(result.details), Self.chutesMonthlyPercentFromDetails(result.details)].compactMap { $0 }
usedPercents = percents.isEmpty ? [singlePercent] : percents
} else if identifier == .nanoGpt {
let percents = [
Expand Down Expand Up @@ -2304,7 +2312,7 @@
for account in geminiAccounts {
hasQuota = true
let accountNumber = account.accountIndex + 1
let usedPercent = normalizedUsagePercent(100.0 - account.remainingPercentage) ?? 0.0
let usedPercent = Self.normalizedUsagePercent(100.0 - account.remainingPercentage) ?? 0.0
// Gemini account rows should represent Gemini quota only.
// Antigravity has its own provider row and should not be duplicated here.
let usedPercents: [Double] = [usedPercent]
Expand Down Expand Up @@ -3081,6 +3089,8 @@
image = NSImage(named: "TavilyIcon")
case .braveSearch:
image = NSImage(named: "BraveSearchIcon")
case .deepSeek:
image = NSImage(systemSymbolName: identifier.iconName, accessibilityDescription: identifier.displayName)
}

// Keep consistent icon sizing and make Gemini slightly larger.
Expand Down Expand Up @@ -3843,7 +3853,7 @@
// 3. OpenRouter - only has current cost, no daily history
// We'll include today's cost if available
if let routerResult = providerResults[.openRouter],
case .payAsYouGo(_, let cost, _) = routerResult.usage,

Check warning on line 3856 in CopilotMonitor/CopilotMonitor/App/StatusBarController.swift

View workflow job for this annotation

GitHub Actions / Build & Test

immutable value 'cost' was never used; consider replacing with '_' or removing it

Check warning on line 3856 in CopilotMonitor/CopilotMonitor/App/StatusBarController.swift

View workflow job for this annotation

GitHub Actions / Build & Test

immutable value 'cost' was never used; consider replacing with '_' or removing it
let dailyCost = routerResult.details?.dailyUsage {
let today = Calendar.current.startOfDay(for: Date())
if aggregatedDailyCosts[today] == nil {
Expand Down
Loading
Loading