Skip to content
Merged
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
17 changes: 17 additions & 0 deletions app/Sources/MenuBarCore/Formatting.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,23 @@ public enum Format {
return "\(max(minutes, 1))m"
}

/// Exact provider-reported reset in the user's local time, with a coarse countdown.
public static func quotaResetDetail(
_ date: Date?, now: Date = Date(),
locale: Locale = .current, timeZone: TimeZone = .current
) -> String {
guard let date, date.timeIntervalSince1970.isFinite else {
return "Reset time unavailable"
}
let formatter = DateFormatter()
formatter.locale = locale
formatter.timeZone = timeZone
formatter.setLocalizedDateFormatFromTemplate("MMMdjmmz")
let timestamp = formatter.string(from: date)
if date <= now { return "Reset time passed · \(timestamp)" }
return "Resets \(timestamp) · in \(resetsIn(date, now: now))"
}

/// "2m ago" for staleness labels on the degraded state.
public static func age(_ date: Date?, now: Date = Date()) -> String {
guard let date else { return unknown }
Expand Down
34 changes: 34 additions & 0 deletions app/Sources/MenuBarCore/ProxyModels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -411,10 +411,28 @@ public struct ProviderQuota: Decodable, Equatable, Sendable {
public let updatedAt: Double
}

/// Pool percentages and the effective subscription's reset schedule have different scopes.
public struct QuotaAggregation: Decodable, Equatable, Sendable {
public let presentation: String?
public let currentAccount: CurrentAccount?

public struct CurrentAccount: Decodable, Equatable, Sendable {
public let quota: ResetSchedule?
}

public struct ResetSchedule: Decodable, Equatable, Sendable {
public let fiveHourResetAt: Double?
public let weeklyResetAt: Double?
public let monthlyResetAt: Double?
public let customWindows: [QuotaWindow]?
}
}

public struct QuotaReport: Decodable, Equatable, Sendable {
public let provider: String
public let label: String
public let source: String
public let aggregation: QuotaAggregation?
public let quota: ProviderQuota
public let updatedAt: Double
}
Expand Down Expand Up @@ -551,6 +569,22 @@ public extension QuotaReport {
Self.date(from: quota.updatedAt)
}

/// Never attach an individual subscription's reset to the combined pool quota.
/// The expanded panel labels this schedule explicitly as the current account.
func currentAccountResetAt(for window: NormalizedQuota) -> Date? {
guard ["openai", "chatgpt"].contains(provider.lowercased()),
aggregation?.presentation == "aggregate",
let schedule = aggregation?.currentAccount?.quota else { return nil }
let value: Double?
switch window.windowLabel {
case "5h": value = schedule.fiveHourResetAt
case "week": value = schedule.weeklyResetAt
case "month": value = schedule.monthlyResetAt
default: value = schedule.customWindows?.first { $0.label == window.windowLabel }?.resetAt
}
return Self.date(from: value)
}

/// Every window the provider reported, in display order.
///
/// The live proxy is not uniform: `openai` and `xai` report a single named window,
Expand Down
17 changes: 17 additions & 0 deletions app/Sources/MenuBarCoreTests/FormattingSuite.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,23 @@ enum FormattingSuite {
t.equal(Format.resetsIn(nil), "—")
}

t.test("format: reset detail includes local date, time, zone, and honest state") {
let now = Date(timeIntervalSince1970: 1_784_915_000)
let reset = now.addingTimeInterval(3 * 86400 + 4 * 3600)
let locale = Locale(identifier: "en_US_POSIX")
let utc = TimeZone(secondsFromGMT: 0)!
let text = Format.quotaResetDetail(reset, now: now, locale: locale, timeZone: utc)
t.expect(text.contains("Jul") && text.contains(":"), "date and clock time are visible")
t.expect(text.contains("GMT"), "time zone is visible")
t.expect(text.hasSuffix(" · in 3d 4h"), "countdown accompanies exact time")
let pacific = Format.quotaResetDetail(reset, now: now, locale: locale,
timeZone: TimeZone(identifier: "America/Los_Angeles")!)
t.expect(pacific != text, "reset follows the supplied local time zone")
t.equal(Format.quotaResetDetail(nil), "Reset time unavailable")
t.expect(Format.quotaResetDetail(now, now: now).hasPrefix("Reset time passed · "),
"past resets do not claim a future renewal")
}

t.test("format: staleness ages read naturally") {
let now = Date(timeIntervalSince1970: 1_784_915_000)
t.equal(Format.age(now.addingTimeInterval(-10), now: now), "just now")
Expand Down
32 changes: 32 additions & 0 deletions app/Sources/MenuBarCoreTests/ModelDecodingSuite.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,38 @@ enum ModelDecodingSuite {
"""

static func run(_ t: TestRunner) {
t.test("quota: pool account resets remain separate from aggregate percentages") {
let reset: Double = 1_900_000_000
let json = """
{"provider":"openai","label":"ChatGPT","source":"chatgpt:wham","updatedAt":1,
"quota":{"weeklyPercent":29,"monthlyPercent":10,"updatedAt":1},
"aggregation":{"presentation":"aggregate","includedAccounts":2,
"weekly":{"nextRecoveryAt":1800000000000},
"currentAccount":{"quota":{"weeklyPercent":50,"weeklyResetAt":\(reset),
"monthlyResetAt":\(reset * 1000)}}}}
"""
let report = try decode(QuotaReport.self, json)
let windows = report.normalizedWindows()
t.equal(windows[0].percent, 29, "pool percentage stays combined")
t.isNil(windows[0].resetAt, "pool is not given a single subscription's reset")
t.equal(report.currentAccountResetAt(for: windows[0]), Date(timeIntervalSince1970: reset),
"uses account reset, not earlier partial pool recovery")
t.equal(report.currentAccountResetAt(for: windows[1]), Date(timeIntervalSince1970: reset),
"both timestamp units are supported")
}

t.test("quota: missing and nonaggregate account schedules do not invent resets") {
for aggregation in ["null", "{}", "{\"presentation\":\"aggregate\",\"currentAccount\":{\"quota\":null}}",
"{\"presentation\":\"coverage-only\",\"currentAccount\":{\"quota\":{\"weeklyResetAt\":1900000000}}}"] {
let report = try decode(QuotaReport.self, """
{"provider":"openai","label":"ChatGPT","source":"test","updatedAt":1,
"quota":{"weeklyPercent":29,"updatedAt":1},"aggregation":\(aggregation)}
""")
t.isNil(report.currentAccountResetAt(for: report.normalizedWindows()[0]),
"absent or inapplicable metadata stays unknown")
}
}

t.test("build provenance: formats only stamped git revisions") {
t.equal(
BuildProvenance.shortRevision(
Expand Down
6 changes: 5 additions & 1 deletion app/Sources/MenuBarUI/ProviderListView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -359,7 +359,11 @@ final class ProviderQuotaRowView: NSView {
arranged.append(ObservedLimitEventRowView(event: limitEvent))
}
for window in windows {
arranged.append(QuotaWindowRowView(window: window))
arranged.append(QuotaWindowRowView(
window: window,
showsResetDetail: ["openai", "chatgpt"].contains(row.provider.lowercased()),
currentAccountResetAt: report.currentAccountResetAt(for: window)
))
}
for reference in references {
arranged.append(ReferenceQuotaWindowRowView(window: reference))
Expand Down
44 changes: 35 additions & 9 deletions app/Sources/MenuBarUI/Views.swift
Original file line number Diff line number Diff line change
Expand Up @@ -656,7 +656,11 @@ final class QuotaBarView: NSView {

/// One real quota window row inside an expanded provider card.
final class QuotaWindowRowView: NSView {
init(window: NormalizedQuota) {
init(
window: NormalizedQuota,
showsResetDetail: Bool = false,
currentAccountResetAt: Date? = nil
) {
super.init(frame: .zero)

let title = makeLabel(windowTitle(window.windowLabel), font: Theme.caption, color: Theme.text)
Expand All @@ -671,7 +675,7 @@ final class QuotaWindowRowView: NSView {
used.translatesAutoresizingMaskIntoConstraints = false
used.widthAnchor.constraint(equalToConstant: 58).isActive = true
let reset = makeLabel(
resetText(window.resetAt, windowLabel: window.windowLabel),
currentAccountResetAt != nil ? "Account reset" : resetText(window.resetAt, windowLabel: window.windowLabel),
font: Theme.micro,
color: Theme.faint
)
Expand All @@ -684,23 +688,45 @@ final class QuotaWindowRowView: NSView {

let row = makeRow([title, bar, used, reset], spacing: 6)
row.translatesAutoresizingMaskIntoConstraints = false
addSubview(row)
let content = NSStackView(views: [row])
content.orientation = .vertical
content.alignment = .leading
content.spacing = 2
content.translatesAutoresizingMaskIntoConstraints = false
let detail: String?
if showsResetDetail, let currentAccountResetAt {
detail = Format.quotaResetDetail(currentAccountResetAt)
} else {
detail = showsResetDetail ? Format.quotaResetDetail(window.resetAt) : nil
}
if let detail {
let label = makeLabel(detail, font: Theme.micro, color: Theme.muted)
label.lineBreakMode = .byWordWrapping
label.maximumNumberOfLines = 0
label.preferredMaxLayoutWidth = Theme.width - Theme.gutter * 2 - 20
content.addArrangedSubview(label)
label.widthAnchor.constraint(equalTo: content.widthAnchor).isActive = true
}
addSubview(content)
NSLayoutConstraint.activate([
row.topAnchor.constraint(equalTo: topAnchor),
row.leadingAnchor.constraint(equalTo: leadingAnchor),
row.trailingAnchor.constraint(equalTo: trailingAnchor),
row.bottomAnchor.constraint(equalTo: bottomAnchor),
content.topAnchor.constraint(equalTo: topAnchor),
content.leadingAnchor.constraint(equalTo: leadingAnchor),
content.trailingAnchor.constraint(equalTo: trailingAnchor),
content.bottomAnchor.constraint(equalTo: bottomAnchor),
row.widthAnchor.constraint(equalTo: content.widthAnchor),
bar.widthAnchor.constraint(greaterThanOrEqualToConstant: 78),
bar.heightAnchor.constraint(equalToConstant: 5),
heightAnchor.constraint(greaterThanOrEqualToConstant: 20),
])

setAccessibilityElement(true)
setAccessibilityRole(.staticText)
let resetDescription = (currentAccountResetAt != nil ? "Current account, " : "")
+ (detail ?? resetText(window.resetAt, windowLabel: window.windowLabel))
setAccessibilityLabel(
window.hasPercent
? "\(windowTitle(window.windowLabel)): \(Format.percent(window.percent)) used, \(resetText(window.resetAt, windowLabel: window.windowLabel))"
: "\(windowTitle(window.windowLabel)): unavailable"
? "\(windowTitle(window.windowLabel)): \(Format.percent(window.percent)) used, \(resetDescription)"
: "\(windowTitle(window.windowLabel)): unavailable, \(resetDescription)"
)
}

Expand Down
45 changes: 45 additions & 0 deletions app/Sources/MenuBarUITests/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -654,6 +654,51 @@ runner.test("ui: ChatGPT/OpenAI expands first; Kimi and Grok stay collapsed") {
runner.equal(accordion.expandedProviderIDs, ["kimi"], "one open section")
}

runner.test("ui: expanded ChatGPT windows show exact resets and collapse hides them") {
let reset = Date().addingTimeInterval(3 * 86400).timeIntervalSince1970
let quotas = decodeQuotas("""
[{"provider":"openai","label":"ChatGPT","source":"test","updatedAt":1,
"quota":{"updatedAt":1,"fiveHourPercent":12,"fiveHourResetAt":\(reset),
"weeklyPercent":22,"weeklyResetAt":\(reset * 1000),"monthlyPercent":30}}]
""")
let accordion = ProviderQuotaAccordionView()
accordion.apply(makeSnapshot(quotas: quotas))
func details() -> [String] {
textFields(in: accordion).map(\.stringValue).filter {
$0.contains(" · in ") || $0 == "Reset time unavailable"
}
}
runner.equal(details().count, 3, "each window has reset detail, including missing times")
runner.equal(details()[0], details()[1], "seconds and milliseconds produce the same reset")
runner.expect(details()[0].contains(":"), "clock time is visible")
accordion.toggleForTesting("openai")
runner.equal(details().count, 0, "collapsed provider hides reset details")
accordion.toggleForTesting("openai")
runner.equal(details().count, 3, "expanding restores reset detail")
}

runner.test("ui: pool quota reads the current subscription reset without changing pool usage") {
let reset = Date().addingTimeInterval(86400).timeIntervalSince1970
let quotas = decodeQuotas("""
[{"provider":"openai","label":"ChatGPT","source":"chatgpt:wham","updatedAt":1,
"quota":{"weeklyPercent":29,"updatedAt":1},
"aggregation":{"presentation":"aggregate","includedAccounts":2,
"weekly":{"nextRecoveryAt":\((reset - 3600) * 1000)},
"currentAccount":{"quota":{"weeklyPercent":50,"weeklyResetAt":\(reset)}}}}]
""")
let accordion = ProviderQuotaAccordionView()
accordion.apply(makeSnapshot(quotas: quotas))
let fields = textFields(in: accordion).map(\.stringValue)
runner.expect(fields.contains("29% used"), "combined pool usage is retained")
runner.expect(fields.contains(Format.quotaResetDetail(Date(timeIntervalSince1970: reset))),
"account reset uses the live nested response format and labels its scope")
runner.expect(!fields.contains("Reset time unavailable"), "known subscription reset is visible")
runner.expect(fields.contains("Account reset"), "summary does not claim the whole pool resets")
accordion.toggleForTesting("openai")
runner.expect(!textFields(in: accordion).contains { $0.stringValue.hasPrefix("Resets ") },
"collapse hides the subscription detail")
}

runner.test("ui: configured Grok stays visible when its quota report is unavailable") {
let quotas = decodeQuotas("""
[
Expand Down
12 changes: 11 additions & 1 deletion app/Sources/UIProbe/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ private enum Fixture {
}
"""

let quotasJSON = """
var quotasJSON = """
[
{
"provider": "openai",
Expand Down Expand Up @@ -250,6 +250,16 @@ private enum Fixture {
]
"""

if CommandLine.arguments.contains("--pool-quota") {
quotasJSON = """
[{"provider":"openai","label":"ChatGPT","source":"chatgpt:wham","updatedAt":\(ms),
"quota":{"weeklyPercent":29,"updatedAt":\(ms)},
"aggregation":{"presentation":"aggregate","includedAccounts":1,"excludedAccounts":0,
"currentAccount":{"quota":{"weeklyPercent":29,
"weeklyResetAt":\(now.addingTimeInterval(86400).timeIntervalSince1970)}}}}]
"""
}

// Grok is intentionally configured but absent from quota reports: this is the
// expired/transient-probe state that must remain visible without a fake value.
let providersJSON = """
Expand Down
Binary file added docs-site/public/macos-quota-resets.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 7 additions & 1 deletion docs-site/src/content/docs/guides/macos-menu-bar.md
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,13 @@ remains intact. Unrelated Codex preference edits made after sync are allowed. If
changed, custom, ambiguous, or cannot be proved safely, CodexCommander leaves the existing route
unchanged rather than guessing. Do not delete or edit the journal manually.

ChatGPT appears first and expanded when its quota report is available. Kimi and Grok appear as
ChatGPT appears first and expanded when its quota report is available. Each expanded ChatGPT quota
window shows its provider-reported reset date and time in your Mac's time zone, plus a countdown.
In Pool mode, percentages describe combined capacity. The reset detail is labeled **Account reset**
and uses the effective subscription's reset schedule; other accounts can reset at different times.
Missing reset times are shown as **Reset time unavailable**; a past timestamp says **Reset time passed**
until refreshed quota data arrives. These are usage-limit resets, not subscription billing dates.
Kimi and Grok appear as
collapsed summaries. A configured quota-capable provider that returns no report stays visible as
**Quota unavailable**; expand it to open Provider settings. When a linked Grok or Kimi CLI login is
stale, the row instead says **Login needs refresh** and tells you to run `grok` or `kimi`, then use
Expand Down
Loading