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
78 changes: 78 additions & 0 deletions Sources/CodexBar/MenuCardView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -649,6 +649,8 @@ struct UsageMenuCardExtraUsageSectionView: View {
// MARK: - Model factory

extension UsageMenuCardView.Model {
private static let miniMaxMetricLimit = 3

struct Input {
let provider: UsageProvider
let metadata: ProviderMetadata
Expand Down Expand Up @@ -771,6 +773,13 @@ extension UsageMenuCardView.Model {
}

private static func usageNotes(input: Input) -> [String] {
if input.provider == .minimax {
return self.miniMaxUsageNotes(
displayEntries: self.miniMaxDisplayEntries(snapshot: input.snapshot),
style: input.resetTimeDisplayStyle,
now: input.now)
}

if input.provider == .kilo {
var notes = Self.kiloLoginDetails(snapshot: input.snapshot)
let resolvedSource = input.sourceLabel?
Expand Down Expand Up @@ -798,6 +807,67 @@ extension UsageMenuCardView.Model {
}
}

private struct MiniMaxDisplayEntries {
let metricEntries: [MiniMaxModelUsageEntry]
let noteEntries: [MiniMaxModelUsageEntry]
}

private static func miniMaxDisplayEntries(snapshot: UsageSnapshot?) -> MiniMaxDisplayEntries {
guard let modelEntries = snapshot?.minimaxUsage?.modelEntries, !modelEntries.isEmpty else {
return MiniMaxDisplayEntries(metricEntries: [], noteEntries: [])
}

var metricEntries: [MiniMaxModelUsageEntry] = []
var noteEntries: [MiniMaxModelUsageEntry] = []

// Note: model entries that cannot compute usage or lack a reset time are silently skipped.
// This is intentional — such entries typically indicate invalid data from the backend and need not be shown.
for entry in modelEntries {
if metricEntries.count < Self.miniMaxMetricLimit,
entry.normalizedSessionUsage() != nil
{
metricEntries.append(entry)
} else {
noteEntries.append(entry)
}
}

return MiniMaxDisplayEntries(metricEntries: metricEntries, noteEntries: noteEntries)
}

private static func miniMaxUsageNotes(
displayEntries: MiniMaxDisplayEntries,
style: ResetTimeDisplayStyle,
now: Date) -> [String]
{
displayEntries.noteEntries.compactMap { entry in
guard let resetText = entry.resetText(style: style, now: now) else { return nil }
return "\(entry.modelName): \(resetText)"
}
}

private static func miniMaxMetrics(input: Input, displayEntries: MiniMaxDisplayEntries) -> [Metric] {
guard !displayEntries.metricEntries.isEmpty else { return [] }
let percentStyle: PercentStyle = input.usageBarsShowUsed ? .used : .left

return displayEntries.metricEntries.enumerated().compactMap { index, entry in
guard let usage = entry.normalizedSessionUsage() else { return nil }
let value = input.usageBarsShowUsed ? usage.used : usage.remaining
let percent = self.clamped((Double(value) / Double(usage.total)) * 100)
return Metric(
id: "minimax-\(index)",
title: entry.modelName,
percent: percent,
percentStyle: percentStyle,
resetText: entry.resetText(style: input.resetTimeDisplayStyle, now: input.now),
detailText: nil,
detailLeftText: nil,
detailRightText: nil,
pacePercent: nil,
paceOnTop: true)
}
}

private static func email(
for provider: UsageProvider,
snapshot: UsageSnapshot?,
Expand Down Expand Up @@ -932,6 +1002,14 @@ extension UsageMenuCardView.Model {

private static func metrics(input: Input) -> [Metric] {
guard let snapshot = input.snapshot else { return [] }
if input.provider == .minimax {
let miniMaxMetrics = self.miniMaxMetrics(
input: input,
displayEntries: self.miniMaxDisplayEntries(snapshot: input.snapshot))
if !miniMaxMetrics.isEmpty {
return miniMaxMetrics
}
}
if input.provider == .antigravity {
return Self.antigravityMetrics(input: input, snapshot: snapshot)
}
Expand Down
12 changes: 11 additions & 1 deletion Sources/CodexBar/MenuDescriptor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,9 @@ struct MenuDescriptor {
.appendActionMenuEntries(context: actionContext, entries: &entries)
}

if metadata?.dashboardURL != nil {
if let targetProvider,
Self.dashboardURL(for: targetProvider, store: store) != nil
{
entries.append(.action("Usage Dashboard", .dashboard))
}
if metadata?.statusPageURL != nil || metadata?.statusLinkURL != nil {
Expand Down Expand Up @@ -407,6 +409,14 @@ struct MenuDescriptor {
return false
}

private static func dashboardURL(for provider: UsageProvider, store: UsageStore) -> URL? {
let context = ProviderDashboardContext(
provider: provider,
settings: store.settings,
store: store)
return ProviderCatalog.implementation(for: provider)?.dashboardURL(context: context)
}

private static func appendRateWindow(
entries: inout [Entry],
title: String,
Expand Down
2 changes: 1 addition & 1 deletion Sources/CodexBar/PreferencesProvidersPane.swift
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ struct ProvidersPane: View {
}

func menuBarMetricPicker(for provider: UsageProvider) -> ProviderSettingsPickerDescriptor? {
if provider == .zai { return nil }
if provider == .zai || provider == .minimax { return nil }
let options: [ProviderSettingsPickerOption]
if provider == .openrouter {
options = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ struct AlibabaCodingPlanProviderImplementation: ProviderImplementation {
_ = settings.alibabaCodingPlanAPIRegion
}

@MainActor
func dashboardURL(context: ProviderDashboardContext) -> URL? {
context.settings.alibabaCodingPlanAPIRegion.dashboardURL
}

@MainActor
func settingsSnapshot(context: ProviderSettingsSnapshotContext) -> ProviderSettingsSnapshotContribution? {
_ = context
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import CodexBarCore
import CodexBarMacroSupport
import Foundation
import SwiftUI

@ProviderImplementationRegistration
Expand Down Expand Up @@ -28,6 +29,27 @@ struct ClaudeProviderImplementation: ProviderImplementation {
_ = settings.claudeWebExtrasEnabled
}

@MainActor
func dashboardURL(context: ProviderDashboardContext) -> URL? {
let meta = context.store.metadata(for: context.provider)
let snapshot = context.store.snapshot(for: context.provider)
let loginMethod = snapshot?.loginMethod(for: context.provider)
let sourceLabel = context.store.sourceLabel(for: context.provider)
// For Claude, route subscription users to claude.ai/settings/usage instead of console billing
let urlString = if Self.prefersClaudeAppDashboard(
loginMethod: loginMethod,
sourceLabel: sourceLabel,
providerCost: snapshot?.providerCost)
{
meta.subscriptionDashboardURL ?? meta.dashboardURL
} else {
meta.dashboardURL
}

guard let urlString else { return nil }
return URL(string: urlString)
}

@MainActor
func settingsSnapshot(context: ProviderSettingsSnapshotContext) -> ProviderSettingsSnapshotContribution? {
.claude(context.settings.claudeSettingsSnapshot(tokenOverride: context.tokenOverride))
Expand Down Expand Up @@ -233,4 +255,39 @@ struct ClaudeProviderImplementation: ProviderImplementation {
}
return false
}

/// Login methods that indicate a consumer/web-based Claude session
private static let consumerLoginMethods: Set<String> = [
ClaudeUsageDataSource.web.rawValue,
"profile",
"browser profile",
]

private static func normalized(_ text: String?) -> String {
text?.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() ?? ""
}

private static func prefersClaudeAppDashboard(
loginMethod: String?,
sourceLabel: String,
providerCost: ProviderCostSnapshot? = nil) -> Bool
{
if ClaudePlan.isSubscriptionLoginMethod(loginMethod) {
return true
}

let normalizedLogin = Self.normalized(loginMethod)
if Self.consumerLoginMethods.contains(normalizedLogin) {
return true
}

// Quota currency code is a strong signal of a subscription plan
if providerCost?.currencyCode == "Quota" {
return true
}

let normalizedSource = Self.normalized(sourceLabel)
return normalizedSource == ClaudeUsageDataSource.web.rawValue
|| normalizedSource == "oauth"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ struct MiniMaxProviderImplementation: ProviderImplementation {
}
}

@MainActor
func dashboardURL(context: ProviderDashboardContext) -> URL? {
context.settings.minimaxAPIRegion.codingPlanURL
}

@MainActor
func observeSettings(_ settings: SettingsStore) {
_ = settings.minimaxCookieSource
Expand Down Expand Up @@ -111,6 +116,15 @@ struct MiniMaxProviderImplementation: ProviderImplementation {
context.settings.minimaxAuthMode()
}

let openDashboard: @MainActor () -> Void = {
let dashboardContext = ProviderDashboardContext(
provider: context.provider,
settings: context.settings,
store: context.store)
guard let url = self.dashboardURL(context: dashboardContext) else { return }
NSWorkspace.shared.open(url)
}

return [
ProviderSettingsFieldDescriptor(
id: "minimax-api-token",
Expand All @@ -125,9 +139,7 @@ struct MiniMaxProviderImplementation: ProviderImplementation {
title: "Open Coding Plan",
style: .link,
isVisible: nil,
perform: {
NSWorkspace.shared.open(context.settings.minimaxAPIRegion.codingPlanURL)
}),
perform: openDashboard),
],
isVisible: nil,
onActivate: { context.settings.ensureMiniMaxAPITokenLoaded() }),
Expand All @@ -144,14 +156,32 @@ struct MiniMaxProviderImplementation: ProviderImplementation {
title: "Open Coding Plan",
style: .link,
isVisible: nil,
perform: {
NSWorkspace.shared.open(context.settings.minimaxAPIRegion.codingPlanURL)
}),
perform: openDashboard),
],
isVisible: {
authMode().allowsCookies && context.settings.minimaxCookieSource == .manual
},
onActivate: { context.settings.ensureMiniMaxCookieLoaded() }),
]
}

@MainActor
func appendUsageMenuEntries(context: ProviderMenuUsageContext, entries: inout [ProviderMenuEntry]) {
guard let modelEntries = context.snapshot?.minimaxUsage?.modelEntries, !modelEntries.isEmpty else { return }
let resetStyle = context.settings.resetTimeDisplayStyle

for modelEntry in modelEntries {
guard let line = Self.modelUsageLine(modelEntry, style: resetStyle) else { continue }
entries.append(.text(line, .secondary))
}
}

private static func modelUsageLine(
_ entry: MiniMaxModelUsageEntry,
style: ResetTimeDisplayStyle,
now: Date = Date()) -> String?
{
guard let detail = entry.resetText(style: style, now: now) else { return nil }
return "\(entry.modelName): \(detail)"
}
}
6 changes: 6 additions & 0 deletions Sources/CodexBar/Providers/Shared/ProviderContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ struct ProviderSourceModeContext {
let settings: SettingsStore
}

struct ProviderDashboardContext {
let provider: UsageProvider
let settings: SettingsStore
let store: UsageStore
}

struct ProviderVersionContext {
let provider: UsageProvider
let browserDetection: BrowserDetection
Expand Down
11 changes: 11 additions & 0 deletions Sources/CodexBar/Providers/Shared/ProviderImplementation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ protocol ProviderImplementation: Sendable {
@MainActor
func sourceMode(context: ProviderSourceModeContext) -> ProviderSourceMode

@MainActor
func dashboardURL(context: ProviderDashboardContext) -> URL?

func detectVersion(context: ProviderVersionContext) async -> String?

func makeRuntime() -> (any ProviderRuntime)?
Expand Down Expand Up @@ -110,6 +113,14 @@ extension ProviderImplementation {
.auto
}

@MainActor
func dashboardURL(context: ProviderDashboardContext) -> URL? {
let meta = context.store.metadata(for: context.provider)
let urlString = meta.dashboardURL
guard let urlString else { return nil }
return URL(string: urlString)
}

func detectVersion(context: ProviderVersionContext) async -> String? {
let detector = ProviderDescriptorRegistry.descriptor(for: self.id).cli.versionDetector
return detector?(context.browserDetection)
Expand Down
18 changes: 5 additions & 13 deletions Sources/CodexBar/StatusItemController+Actions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,19 +40,11 @@ extension StatusItemController {
}

func dashboardURL(for provider: UsageProvider) -> URL? {
if provider == .alibaba {
return self.settings.alibabaCodingPlanAPIRegion.dashboardURL
}

let meta = self.store.metadata(for: provider)
let urlString: String? = if provider == .claude, self.store.isClaudeSubscription() {
meta.subscriptionDashboardURL ?? meta.dashboardURL
} else {
meta.dashboardURL
}

guard let urlString else { return nil }
return URL(string: urlString)
let context = ProviderDashboardContext(
provider: provider,
settings: self.settings,
store: self.store)
return ProviderCatalog.implementation(for: provider)?.dashboardURL(context: context)
}

@objc func openCreditsPurchase() {
Expand Down
Loading