Skip to content
Closed
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
32 changes: 25 additions & 7 deletions Sources/CodexBar/MenuCardView+ModelHelpers.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
import CodexBarCore
import SwiftUI

/// Resolved display labels for a provider's session/weekly/model-specific/Fable rate windows.
struct RateWindowLabels {
let primary: String
let secondary: String
let tertiary: String
let showsTertiary: Bool
let quaternary: String
let showsQuaternary: Bool
}

extension UsageMenuCardView.Model {
struct PaceDetail {
let leftLabel: String
Expand Down Expand Up @@ -218,10 +228,16 @@ extension UsageMenuCardView.Model {

static func rateWindowLabels(
input: Input,
snapshot: UsageSnapshot) -> (primary: String, secondary: String, tertiary: String, showsTertiary: Bool)
snapshot: UsageSnapshot) -> RateWindowLabels
{
if input.provider == .factory, snapshot.tertiary != nil {
return ("5-hour", L("Weekly"), L("Monthly"), true)
return RateWindowLabels(
primary: "5-hour",
secondary: L("Weekly"),
tertiary: L("Monthly"),
showsTertiary: true,
quaternary: "Fable",
showsQuaternary: false)
}
// Legacy request-based Cursor plans track a request quota, not the token-based "Total" pool.
let primaryLabel = if input.provider == .cursor, snapshot.cursorRequests != nil {
Expand All @@ -231,11 +247,13 @@ extension UsageMenuCardView.Model {
} else {
input.metadata.sessionLabel
}
return (
L(primaryLabel),
L(input.metadata.weeklyLabel),
input.metadata.opusLabel.map(L) ?? L("Sonnet"),
input.metadata.supportsOpus)
return RateWindowLabels(
primary: L(primaryLabel),
secondary: L(input.metadata.weeklyLabel),
tertiary: input.metadata.opusLabel.map(L) ?? L("Sonnet"),
showsTertiary: input.metadata.supportsOpus,
quaternary: input.metadata.fableLabel.map(L) ?? L("Fable"),
showsQuaternary: input.metadata.supportsFable)
}

static func resetText(
Expand Down
16 changes: 16 additions & 0 deletions Sources/CodexBar/MenuCardView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1178,6 +1178,22 @@ extension UsageMenuCardView.Model {
thresholds: input.quotaWarningThresholds[.weekly],
showUsed: input.usageBarsShowUsed)))
}
if labels.showsQuaternary, let fable = snapshot.quaternary {
metrics.append(Metric(
id: "quaternary",
title: labels.quaternary,
percent: Self.clamped(input.usageBarsShowUsed ? fable.usedPercent : fable.remainingPercent),
percentStyle: percentStyle,
resetText: Self.resetText(for: fable, style: input.resetTimeDisplayStyle, now: input.now),
detailText: nil,
detailLeftText: nil,
detailRightText: nil,
pacePercent: nil,
paceOnTop: true,
warningMarkerPercents: Self.warningMarkerPercents(
thresholds: input.quotaWarningThresholds[.weekly],
showUsed: input.usageBarsShowUsed)))
}
metrics.append(contentsOf: Self.extraRateWindowMetrics(
snapshot: snapshot,
input: input,
Expand Down
30 changes: 23 additions & 7 deletions Sources/CodexBar/MenuDescriptor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,14 @@ struct MenuDescriptor {
showUsed: settings.usageBarsShowUsed,
resetOverride: opusResetOverride)
}
if labels.showsQuaternary, let fable = snap.quaternary {
Self.appendRateWindow(
entries: &entries,
title: labels.quaternary,
window: fable,
resetStyle: resetStyle,
showUsed: settings.usageBarsShowUsed)
}

Self.appendProviderUsageSummaries(entries: &entries, snapshot: snap)
if snap.rateLimitsUnavailable(for: provider) {
Expand Down Expand Up @@ -708,19 +716,27 @@ struct MenuDescriptor {
private static func rateWindowLabels(
provider: UsageProvider,
metadata: ProviderMetadata,
snapshot: UsageSnapshot) -> (primary: String, secondary: String, tertiary: String, showsTertiary: Bool)
snapshot: UsageSnapshot) -> RateWindowLabels
{
if provider == .factory, snapshot.tertiary != nil {
return ("5-hour", L("Weekly"), L("Monthly"), true)
return RateWindowLabels(
primary: "5-hour",
secondary: L("Weekly"),
tertiary: L("Monthly"),
showsTertiary: true,
quaternary: "Fable",
showsQuaternary: false)
}
let primaryLabel = provider == .grok
? GrokProviderDescriptor.primaryLabel(window: snapshot.primary) ?? metadata.sessionLabel
: metadata.sessionLabel
return (
L(primaryLabel),
L(metadata.weeklyLabel),
metadata.opusLabel.map(L) ?? L("Sonnet"),
metadata.supportsOpus)
return RateWindowLabels(
primary: L(primaryLabel),
secondary: L(metadata.weeklyLabel),
tertiary: metadata.opusLabel.map(L) ?? L("Sonnet"),
showsTertiary: metadata.supportsOpus,
quaternary: metadata.fableLabel.map(L) ?? L("Fable"),
showsQuaternary: metadata.supportsFable)
}

private static func appendRateWindow(
Expand Down
9 changes: 9 additions & 0 deletions Sources/CodexBar/PlanUtilizationHistoryChartMenuView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,11 @@ struct PlanUtilizationHistoryChartMenuView: View {
{
names.insert(.opus)
}
if snapshot.quaternary != nil,
ProviderDescriptorRegistry.metadata[provider]?.supportsFable == true
{
names.insert(.fable)
}
default:
let windows = [snapshot.primary, snapshot.secondary, snapshot.tertiary].compactMap(\.self)
+ (snapshot.extraRateWindows?.filter(\.usageKnown).map(\.window) ?? [])
Expand Down Expand Up @@ -623,6 +628,8 @@ struct PlanUtilizationHistoryChartMenuView: View {
L(metadata?.weeklyLabel ?? "Weekly")
case .opus:
metadata?.opusLabel ?? "Opus"
case .fable:
metadata?.fableLabel ?? "Fable"
default:
self.fallbackTitle(for: name.rawValue)
}
Expand All @@ -643,6 +650,8 @@ struct PlanUtilizationHistoryChartMenuView: View {
1
case .opus:
2
case .fable:
3
default:
100
}
Expand Down
3 changes: 3 additions & 0 deletions Sources/CodexBar/PlanUtilizationHistoryStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,16 @@ struct PlanUtilizationSeriesName: RawRepresentable, Hashable, Codable, Expressib
static let session: Self = "session"
static let weekly: Self = "weekly"
static let opus: Self = "opus"
static let fable: Self = "fable"

func canonicalWindowMinutes(_ windowMinutes: Int) -> Int {
switch self {
case .session where (295...305).contains(windowMinutes):
300
case .weekly where (10070...10090).contains(windowMinutes):
10080
case .fable where (10070...10090).contains(windowMinutes):
10080
default:
windowMinutes
}
Expand Down
1 change: 1 addition & 0 deletions Sources/CodexBar/UsageStore+PlanUtilization.swift
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,7 @@ extension UsageStore {
appendWindow(snapshot.primary, name: .session)
appendWindow(snapshot.secondary, name: .weekly)
appendWindow(snapshot.tertiary, name: .opus)
appendWindow(snapshot.quaternary, name: .fable)
case .antigravity:
let namedWeeklyWindows = snapshot.extraRateWindows?
.filter {
Expand Down
7 changes: 7 additions & 0 deletions Sources/CodexBar/UsageStore+WidgetSnapshot.swift
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ extension UsageStore {
primary: snapshot?.primary,
secondary: snapshot?.secondary,
tertiary: snapshot?.tertiary,
quaternary: snapshot?.quaternary,
usageRows: usageRows,
creditsRemaining: creditsRemaining,
codeReviewRemainingPercent: codeReviewRemaining,
Expand Down Expand Up @@ -165,6 +166,12 @@ extension UsageStore {
title: metadata?.opusLabel ?? "Opus",
percentLeft: snapshot.tertiary?.remainingPercent))
}
if metadata?.supportsFable == true {
rows.append(WidgetSnapshot.WidgetUsageRowSnapshot(
id: "quaternary",
title: metadata?.fableLabel ?? "Fable",
percentLeft: snapshot.quaternary?.remainingPercent))
}
return rows.filter { $0.percentLeft != nil }
}

Expand Down
25 changes: 23 additions & 2 deletions Sources/CodexBarCLI/CLIRenderer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ enum CLIRenderer {
now: now,
lines: &lines)
self.appendTertiaryLines(snapshot: snapshot, labels: labels, context: context, now: now, lines: &lines)
self.appendQuaternaryLines(snapshot: snapshot, labels: labels, context: context, now: now, lines: &lines)
self.appendMiMoBalanceLine(snapshot: snapshot, useColor: context.useColor, lines: &lines)
self.appendDeepgramLines(snapshot: snapshot, useColor: context.useColor, lines: &lines)
self.appendAmpBalanceLines(snapshot: snapshot, useColor: context.useColor, lines: &lines)
Expand Down Expand Up @@ -160,6 +161,20 @@ enum CLIRenderer {
}
}

private static func appendQuaternaryLines(
snapshot: UsageSnapshot,
labels: RateWindowLabels,
context: RenderContext,
now: Date,
lines: inout [String])
{
guard labels.showsQuaternary, let fable = snapshot.quaternary else { return }
lines.append(self.rateLine(title: labels.quaternary, window: fable, useColor: context.useColor))
if let reset = self.resetLine(for: fable, style: context.resetStyle, now: now) {
lines.append(self.subtleLine(reset, useColor: context.useColor))
}
}

private static func appendDeepgramLines(
snapshot: UsageSnapshot,
useColor: Bool,
Expand Down Expand Up @@ -204,6 +219,8 @@ enum CLIRenderer {
let secondary: String
let tertiary: String
let showsTertiary: Bool
let quaternary: String
let showsQuaternary: Bool
}

private static func rateWindowLabels(
Expand All @@ -216,7 +233,9 @@ enum CLIRenderer {
primary: "5-hour",
secondary: "Weekly",
tertiary: "Monthly",
showsTertiary: true)
showsTertiary: true,
quaternary: "Fable",
showsQuaternary: false)
}
let primaryLabel = provider == .grok
? GrokProviderDescriptor.primaryLabel(window: snapshot.primary) ?? metadata.sessionLabel
Expand All @@ -225,7 +244,9 @@ enum CLIRenderer {
primary: primaryLabel,
secondary: metadata.weeklyLabel,
tertiary: metadata.opusLabel ?? "Sonnet",
showsTertiary: metadata.supportsOpus)
showsTertiary: metadata.supportsOpus,
quaternary: metadata.fableLabel ?? "Fable",
showsQuaternary: metadata.supportsFable)
}

private static func appendCreditsLine(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,11 @@ struct OAuthUsageResponse: Decodable {
let sevenDayOAuthApps: OAuthUsageWindow?
let sevenDayOpus: OAuthUsageWindow?
let sevenDaySonnet: OAuthUsageWindow?
let sevenDayFable: OAuthUsageWindow?
/// Model-scoped weekly limit (from the `limits` array) whose model is NOT Fable — e.g. Sonnet/Opus.
/// Used as a fallback for the model-specific weekly window on the newest payload shape, where the
/// legacy top-level `seven_day_sonnet`/`seven_day_opus` keys are null.
let sevenDayModelScoped: OAuthUsageWindow?
let sevenDayRoutines: OAuthUsageWindow?
let sevenDayRoutinesSourceKey: String?
let iguanaNecktie: OAuthUsageWindow?
Expand All @@ -164,6 +169,21 @@ struct OAuthUsageResponse: Decodable {
self.sevenDayOAuthApps = Self.decodeWindow(in: container, keys: ["seven_day_oauth_apps"])
self.sevenDayOpus = Self.decodeWindow(in: container, keys: ["seven_day_opus"])
self.sevenDaySonnet = Self.decodeWindow(in: container, keys: ["seven_day_sonnet"])
// Newest usage payloads move model-scoped weekly limits into a `limits` array (each
// `weekly_scoped` entry carries `scope.model.display_name`, e.g. "Fable"/"Sonnet") and leave the
// legacy top-level `seven_day_*` keys null. Prefer the explicit top-level key when present, else
// fall back to the matching scoped-limit entry so both shapes are supported.
let limits: [OAuthUsageLimit] = Self.decodeValue(in: container, keys: ["limits"]) ?? []
func scopedWeeklyWindow(isFable: Bool) -> OAuthUsageWindow? {
limits.first { limit in
guard limit.kind == "weekly_scoped", let name = limit.modelDisplayName else { return false }
let matchesFable = name.caseInsensitiveCompare("Fable") == .orderedSame
return isFable ? matchesFable : !matchesFable
}?.asWindow
}
self.sevenDayFable = Self.decodeWindow(in: container, keys: ["seven_day_fable"])
?? scopedWeeklyWindow(isFable: true)
self.sevenDayModelScoped = scopedWeeklyWindow(isFable: false)
let routines = Self.decodeWindowWithSource(in: container, keys: [
"seven_day_routines",
"seven_day_claude_routines",
Expand Down Expand Up @@ -242,6 +262,58 @@ struct OAuthUsageWindow: Decodable {
}
}

/// One entry of the newest usage payload's `limits` array. Model-scoped weekly limits carry the human
/// model name via `scope.model.display_name` (e.g. "Fable"), which is the authoritative way to identify
/// the model — the legacy top-level `seven_day_*` keys are null on this payload shape.
struct OAuthUsageLimit: Decodable {
let kind: String?
let group: String?
let percent: Double?
let resetsAt: String?
let isActive: Bool?
let modelDisplayName: String?

private enum CodingKeys: String, CodingKey {
case kind
case group
case percent
case resetsAt = "resets_at"
case isActive = "is_active"
case scope
}

private enum ScopeKeys: String, CodingKey {
case model
}

private enum ModelKeys: String, CodingKey {
case displayName = "display_name"
}

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.kind = try container.decodeIfPresent(String.self, forKey: .kind)
self.group = try container.decodeIfPresent(String.self, forKey: .group)
self.percent = try container.decodeIfPresent(Double.self, forKey: .percent)
self.resetsAt = try container.decodeIfPresent(String.self, forKey: .resetsAt)
self.isActive = try container.decodeIfPresent(Bool.self, forKey: .isActive)
// `scope` is null for session/weekly_all entries; only weekly_scoped carries scope.model.
if let scope = try? container.nestedContainer(keyedBy: ScopeKeys.self, forKey: .scope),
let model = try? scope.nestedContainer(keyedBy: ModelKeys.self, forKey: .model)
{
self.modelDisplayName = try? model.decodeIfPresent(String.self, forKey: .displayName)
} else {
self.modelDisplayName = nil
}
}

/// Adapts this limit into the `OAuthUsageWindow` shape (`utilization` + `resets_at`) so it can flow
/// through the same makeWindow(_:windowMinutes:) mapping as the legacy top-level windows.
var asWindow: OAuthUsageWindow {
OAuthUsageWindow(utilization: self.percent, resetsAt: self.resetsAt)
}
}

struct OAuthExtraUsage: Decodable {
let isEnabled: Bool?
let monthlyLimit: Double?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ public enum ClaudeProviderDescriptor {
weeklyLabel: "Weekly",
opusLabel: "Sonnet",
supportsOpus: true,
fableLabel: "Fable",
supportsFable: true,
supportsCredits: false,
creditsHint: "",
toggleTitle: "Show Claude Code usage",
Expand Down Expand Up @@ -401,6 +403,7 @@ struct ClaudeOAuthFetchStrategy: ProviderFetchStrategy {
primary: primary,
secondary: usage.secondary,
tertiary: usage.opus,
quaternary: usage.fable,
extraRateWindows: usage.extraRateWindows.isEmpty ? nil : usage.extraRateWindows,
providerCost: usage.providerCost,
updatedAt: usage.updatedAt,
Expand Down
Loading