Skip to content
Closed
Show file tree
Hide file tree
Changes from 4 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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

### Added
- Agent Sessions: list and focus live local or SSH-discovered Codex and Claude Code sessions from the menu and CLI.
- MiniMax: fetch Token Plan recharge credit balance from the console when a web session cookie is available, including MiniMax Agent desktop cookies and explicit manual or env cookies on API-token refreshes. Thanks @Yuxin-Qiao!
- MiniMax: show console usage-summary KPIs, cost trends, and token usage details in the menu dashboard. Thanks @Yuxin-Qiao!

### Fixed
- Ollama: recognize current WorkOS AuthKit sessions during browser-cookie discovery and manual cookie validation. Thanks @joeVenner!
Expand Down
251 changes: 248 additions & 3 deletions Sources/CodexBar/InlineUsageDashboardContent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,14 @@ extension UsageMenuCardView.Model {
return notes
}

if input.provider == .minimax,
input.showOptionalCreditsAndExtraUsage,
let usageSummary = input.snapshot?.minimaxUsage?.usageSummary,
usageSummary.hasDisplayableData
{
return Self.minimaxUsageSummaryNotes(usageSummary)
}

if input.provider == .minimax,
input.showOptionalCreditsAndExtraUsage,
let billing = input.snapshot?.minimaxUsage?.billingSummary
Expand Down Expand Up @@ -183,7 +191,21 @@ extension UsageMenuCardView.Model {
}

private static func resolveInlineUsageDashboard(input: Input) -> InlineUsageDashboardModel? {
if input.provider == .minimax,
input.showOptionalCreditsAndExtraUsage,
input.tokenCostInlineDashboardEnabled,
let usageSummary = input.snapshot?.minimaxUsage?.usageSummary,
usageSummary.hasDisplayableData,
let costSnapshot = primaryCostHistorySnapshot(input: input),
!costSnapshot.daily.isEmpty
{
return self.minimaxCostAndUsageSummaryInlineDashboard(
snapshot: costSnapshot,
usage: usageSummary,
comparisonPeriodsEnabled: input.costComparisonPeriodsEnabled)
}
if self.usesProviderCostHistoryAsPrimaryDashboard(input.provider),
input.provider != .minimax || input.tokenCostInlineDashboardEnabled,
let tokenSnapshot = primaryCostHistorySnapshot(input: input),
!tokenSnapshot.daily.isEmpty
Comment on lines 207 to 210

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 Gate MiniMax cost dashboard behind optional-usage toggle

When MiniMax is selected and “Show optional credits and extra usage” is disabled, this generic cost-history branch can still render a MiniMax dashboard as long as token-cost dashboards are enabled and the snapshot already contains minimaxUsage.usageSummary (for example from a previous refresh or cache). The MiniMax-specific summary branches above are guarded by showOptionalCreditsAndExtraUsage, so this path leaks the optional web usage/cost data that the setting is meant to hide.

Useful? React with 👍 / 👎.

{
Expand Down Expand Up @@ -212,6 +234,13 @@ extension UsageMenuCardView.Model {
{
return Self.zaiInlineDashboard(modelUsage: modelUsage, now: input.now)
}
if input.provider == .minimax,
input.showOptionalCreditsAndExtraUsage,
let usageSummary = input.snapshot?.minimaxUsage?.usageSummary,
usageSummary.hasDisplayableData
{
return Self.minimaxUsageSummaryInlineDashboard(usageSummary)
}
if input.provider == .minimax,
input.showOptionalCreditsAndExtraUsage,
let billing = input.snapshot?.minimaxUsage?.billingSummary,
Expand Down Expand Up @@ -246,7 +275,7 @@ extension UsageMenuCardView.Model {
}

static func usesProviderCostHistoryAsPrimaryDashboard(_ provider: UsageProvider) -> Bool {
provider == .openai || provider == .mistral
provider == .openai || provider == .mistral || provider == .minimax
}

static func primaryCostHistorySnapshot(input: Input) -> CostUsageTokenSnapshot? {
Expand All @@ -261,6 +290,11 @@ extension UsageMenuCardView.Model {
return projected
}
return input.snapshot == nil ? input.tokenSnapshot : nil
case .minimax:
if let projected = input.snapshot?.minimaxUsage?.usageSummary?.toCostUsageTokenSnapshot() {
return projected
}
return input.snapshot == nil ? input.tokenSnapshot : nil
default:
return input.tokenSnapshot
}
Expand Down Expand Up @@ -396,7 +430,9 @@ extension UsageMenuCardView.Model {
emphasis: false),
.init(
title: tokenHistoryTitle,
value: snapshot.last30DaysTokens.map(UsageFormatter.tokenCountString) ?? "—",
value: snapshot.last30DaysTokens.map {
Self.tokenCountString($0, provider: provider)
} ?? "—",
emphasis: false),
] + Self.costHistoryTrailingKPIs(snapshot: snapshot, latest: latest),
points: points,
Expand Down Expand Up @@ -638,6 +674,214 @@ extension UsageMenuCardView.Model {
detailLines: details)
}

private static func minimaxUsageSummaryInlineDashboard(_ usage: MiniMaxUsageSummary) -> InlineUsageDashboardModel {
let trendDays = usage.trendDays(last: 7)
let points = trendDays.map {
let tokenText = Self.minimaxTokenString($0.totalToken)
let cacheText = UsageFormatter.optionalPercentString($0.cacheHitPercent)
let cacheLine = String(format: L("Cache hit: %@"), cacheText)
return InlineUsageDashboardModel.Point(
id: $0.date,
label: Self.shortDayLabel($0.date),
value: Double($0.totalToken),
accessibilityValue: "\($0.date): \(tokenText) \(L("tokens")) · \(cacheLine)")
}
let cacheText = UsageFormatter.optionalPercentString(usage.snapshotDay?.cacheHitPercent)
let todayTitle = usage.lastUpdateTime
.flatMap { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
.flatMap { $0.isEmpty ? nil : String(format: L("%@ usage"), $0) }
?? L("Today")

return InlineUsageDashboardModel(
accessibilityLabel: L("MiniMax 7 day token usage trend"),
valueStyle: .tokens,
kpis: [
.init(
title: todayTitle,
value: Self.minimaxTokenString(usage.latestSnapshotTokens),
emphasis: true),
.init(
title: L("7d tokens"),
value: Self.minimaxTokenString(usage.last7DaysTokens),
emphasis: false),
.init(
title: L("Cache hit"),
value: cacheText,
emphasis: false),
.init(
title: L("30d tokens"),
value: Self.minimaxTokenString(usage.last30DaysTokens),
emphasis: false),
],
points: points,
detailLines: Self.minimaxUsageSummaryCardDetailLines(usage))
}

private static func minimaxLatestUsageKPITitle(_ usage: MiniMaxUsageSummary) -> String {
if let time = usage.lastUpdateTime?.trimmingCharacters(in: .whitespacesAndNewlines), !time.isEmpty {
return String(format: L("%@ usage"), time)
}
return L("Latest usage")
}

private static func minimaxCostAndUsageSummaryInlineDashboard(
snapshot: CostUsageTokenSnapshot,
usage: MiniMaxUsageSummary,
comparisonPeriodsEnabled: Bool) -> InlineUsageDashboardModel
{
let base = self.costHistoryInlineDashboard(
provider: .minimax,
snapshot: snapshot,
comparisonPeriodsEnabled: comparisonPeriodsEnabled)
let cacheText = UsageFormatter.optionalPercentString(usage.snapshotDay?.cacheHitPercent)
return InlineUsageDashboardModel(
accessibilityLabel: base.accessibilityLabel,
valueStyle: base.valueStyle,
kpis: [
base.kpis[0],
base.kpis[1],
.init(
title: self.minimaxLatestUsageKPITitle(usage),
value: self.minimaxTokenString(usage.latestSnapshotTokens),
emphasis: false),
.init(
title: L("7d tokens"),
value: self.minimaxTokenString(usage.last7DaysTokens),
emphasis: false),
.init(
title: L("Cache hit"),
value: cacheText,
emphasis: false),
.init(
title: L("30d tokens"),
value: self.minimaxTokenString(usage.last30DaysTokens),
emphasis: false),
],
points: base.points,
detailLines: self.minimaxCostDashboardDetailLines(snapshot: snapshot, usage: usage))
}

private static func minimaxCostDashboardDetailLines(
snapshot: CostUsageTokenSnapshot,
usage: MiniMaxUsageSummary) -> [String]
{
var details: [String] = []
if let topModel = Self.topCostModel(from: snapshot.daily) {
details.append("\(L("Top model")): \(Self.shortModelName(topModel))")
}
if let day = usage.snapshotDay,
let todayLine = Self.minimaxTodayBreakdownLine(day)
{
details.append(todayLine)
}
if let hint = Self.tokenUsageHint(provider: .minimax) {
details.append(hint)
}
details.append(L("Language models only; data may be delayed"))
return details
}

private static func minimaxUsageSummaryCardDetailLines(_ usage: MiniMaxUsageSummary) -> [String] {
var details: [String] = []
if let day = usage.snapshotDay,
let todayLine = Self.minimaxTodayBreakdownLine(day)
{
details.append(todayLine)
}
if let total = usage.totalTokenConsumed, !total.isEmpty {
details.append("\(L("Total consumed")): \(total)")
}
if let cache = usage.snapshotDay?.cacheHitPercent {
details.append(
String(format: L("Cache hit: %@"), UsageFormatter.optionalPercentString(cache)))
}
if let activeDays = usage.activeDays {
details.append(String(format: L("Active days: %@"), "\(activeDays)"))
}
if let streak = usage.currentConsecutiveDays {
details.append(String(format: L("Streak: %@ days"), "\(streak)"))
}
if let rank = usage.usageRankingPercent {
details.append(
String(
format: L("Usage rank: top %@%%"),
UsageFormatter.decimalString(rank)))
}
if let updated = usage.lastUpdateTime, !updated.isEmpty {
details.append("\(L("Updated")): \(updated)")
}
if let topModel = Self.topMiniMaxUsageSummaryModel(from: usage.days) {
details.append("\(L("Top model")): \(Self.shortModelName(topModel.model)) (\(topModel.tokens))")
}
details.append(L("Language models only; data may be delayed"))
return details
}

private static func minimaxTodayBreakdownLine(_ day: MiniMaxUsageSummaryDay) -> String? {
if let topModel = day.models.max(by: { lhs, rhs in
if lhs.totalToken == rhs.totalToken { return lhs.model > rhs.model }
return lhs.totalToken < rhs.totalToken
}) {
let cacheText = UsageFormatter.optionalPercentString(topModel.cacheHitPercent)
return String(
format: L("%@ · %@ input · %@ output · %@ cache hit"),
Self.shortModelName(topModel.model),
Self.minimaxTokenString(topModel.inputToken),
Self.minimaxTokenString(topModel.outputToken),
cacheText)
}
guard day.totalToken > 0 else { return nil }
let cacheText = UsageFormatter.optionalPercentString(day.cacheHitPercent)
return String(
format: L("%@ input · %@ output · %@ cache hit"),
Self.minimaxTokenString(day.totalInputToken),
Self.minimaxTokenString(day.totalOutputToken),
cacheText)
}

private static func minimaxTokenString(_ value: Int) -> String {
UsageFormatter.tokenCountString(value, fractionDigits: 2)
}

private static func tokenCountString(_ value: Int, provider: UsageProvider) -> String {
if provider == .minimax {
return self.minimaxTokenString(value)
}
return UsageFormatter.tokenCountString(value)
}

private static func minimaxUsageSummaryNotes(_ usage: MiniMaxUsageSummary) -> [String] {
[
String(
format: L("Today: %@ tokens"),
self.minimaxTokenString(usage.latestSnapshotTokens)),
String(
format: L("Last 7 days: %@ tokens"),
self.minimaxTokenString(usage.last7DaysTokens)),
String(
format: L("Last 30 days: %@ tokens"),
self.minimaxTokenString(usage.last30DaysTokens)),
] + self.minimaxUsageSummaryCardDetailLines(usage)
}

private static func topMiniMaxUsageSummaryModel(
from days: [MiniMaxUsageSummaryDay]) -> (model: String, tokens: String)?
{
var totals: [String: Int] = [:]
for day in days {
for model in day.models {
totals[model.model, default: 0] += model.totalToken
}
}
guard let top = totals.max(by: { lhs, rhs in
if lhs.value == rhs.value { return lhs.key > rhs.key }
return lhs.value < rhs.value
}) else {
return nil
}
return (top.key, Self.minimaxTokenString(top.value))
}

private static func deepseekInlineDashboard(_ usage: DeepSeekUsageSummary) -> InlineUsageDashboardModel {
let symbol = usage.currency == "CNY" ? "¥" : "$"
let points = usage.daily.suffix(30).map {
Expand Down Expand Up @@ -810,7 +1054,8 @@ struct InlineUsageDashboardContent: View {
Text(line)
.font(.caption)
.foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted))
.lineLimit(1)
.lineLimit(2)
.fixedSize(horizontal: false, vertical: true)
}
}
}
Expand Down
9 changes: 7 additions & 2 deletions Sources/CodexBar/MenuCardView+Costs.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ extension UsageMenuCardView.Model {
error: String?) -> String?
{
guard metadata.supportsCredits else { return nil }
if metadata.id == .codex, credits == nil, error == nil { return nil }
if metadata.id == .amp,
let ampUsage = snapshot?.ampUsage,
let ampCredits = self.ampCreditsLine(ampUsage)
Expand Down Expand Up @@ -182,6 +181,8 @@ extension UsageMenuCardView.Model {
L("Reported by OpenAI Admin API organization usage.")
case .mistral:
L("Reported by Mistral billing usage.")
case .minimax:
L("Estimated from MiniMax usage summary and published pay-as-you-go pricing.")
default:
nil
}
Expand Down Expand Up @@ -304,11 +305,15 @@ extension UsageMenuCardView.Model {

if provider == .minimax, cost.period == "MiniMax points balance" {
let balance = String(format: "%.0f", cost.used)
let expiresLine = cost.resetsAt.map {
String(format: L("Expires: %@"), UsageFormatter.preciseDateTimeDescription(from: $0))
}
return ProviderCostSection(
title: L("Credits"),
percentUsed: nil,
spendLine: "\(L("Balance")): \(balance)",
percentLine: nil)
percentLine: nil,
personalSpendLine: expiresLine)
}

if provider == .openai || provider == .claude || provider == .litellm, cost.limit <= 0 {
Expand Down
20 changes: 19 additions & 1 deletion Sources/CodexBar/MenuCardView+MiniMax.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ extension UsageMenuCardView.Model {
percent: displayPercent,
percentStyle: percentStyle,
statusText: service.isUnlimited ? "∞ Unlimited" : nil,
resetText: Self.localizedMiniMaxResetDescription(service.resetDescription),
resetText: Self.miniMaxResetText(for: service, input: input),
detailText: nil,
detailLeftText: usageLabel,
detailRightText: nil,
Expand Down Expand Up @@ -112,6 +112,24 @@ extension UsageMenuCardView.Model {
return trimmed.isEmpty ? windowType : trimmed
}

private static func miniMaxResetText(for service: MiniMaxServiceUsage, input: Input) -> String {
if service.isUnlimited {
return self.localizedMiniMaxResetDescription(service.resetDescription)
}
if let resetsAt = service.resetsAt {
switch input.resetTimeDisplayStyle {
case .absolute:
let text = UsageFormatter.resetDescription(from: resetsAt, now: input.now)
return L("Resets %@", text)
case .countdown:
let phrase = MiniMaxServiceUsage.resetCountdownPhrase(from: resetsAt, now: input.now)
if phrase == "now" { return L("Resets now") }
return L("Resets in %@", phrase)
}
}
return Self.localizedMiniMaxResetDescription(service.resetDescription)
}

private static func localizedMiniMaxResetDescription(_ text: String) -> String {
let prefix = "Resets in "
guard text.hasPrefix(prefix) else { return text }
Expand Down
Loading
Loading