diff --git a/Sources/CodingBar/SelfTest.swift b/Sources/CodingBar/SelfTest.swift index 6de7f16..0baff91 100644 --- a/Sources/CodingBar/SelfTest.swift +++ b/Sources/CodingBar/SelfTest.swift @@ -153,6 +153,13 @@ enum SelfTest { check("codex usage → 2 windows", codexWindows.count == 2) check("codex secondary labelled 7d", codexWindows.last?.label == "7d") check("codex 7d remaining ~0.26", abs((codexWindows.last?.remaining ?? 0) - 0.26) < 0.0001) + check("codex menu quota uses weekly window", + codexWindows.menuWindow(preferring: .codex)?.label == "7d") + let codexFiveHourOnly = [QuotaWindow(provider: .codex, label: "5h", remaining: 0.91, resetAt: nil)] + let canonicalFallback = [QuotaWindow(provider: .claude, label: "5h", remaining: 0.72, resetAt: nil)] + codexFiveHourOnly + check("codex menu quota never falls back to 5h", + codexFiveHourOnly.menuWindow(preferring: .codex) == nil + && canonicalFallback.menuWindow(preferring: .codex)?.provider == .claude) let mixed = claudeWindows + codexWindows check("tightestRemaining picks most-depleted", abs((mixed.tightestRemaining ?? 1) - 0.26) < 0.0001) diff --git a/Sources/CodingBar/Views/MenuBarItemView.swift b/Sources/CodingBar/Views/MenuBarItemView.swift index af92d9e..8bbc88c 100644 --- a/Sources/CodingBar/Views/MenuBarItemView.swift +++ b/Sources/CodingBar/Views/MenuBarItemView.swift @@ -59,7 +59,7 @@ struct MenuBarReadout: View { .fixedSize() } - // `pct` is the remaining fraction of the menu window (Claude 5h). We render it + // `pct` is the remaining fraction of the selected menu-bar quota window. We render it // as *used %* per user preference; the 4-cell meter lights up with usage and is // colored by health (low usage = green, high usage = red). The Spacer expands // to fill the shared width, so the meter stays flush with the number's edge. diff --git a/Sources/CodingBarCore/Aggregator.swift b/Sources/CodingBarCore/Aggregator.swift index 2ca55fe..11eb36b 100644 --- a/Sources/CodingBarCore/Aggregator.swift +++ b/Sources/CodingBarCore/Aggregator.swift @@ -232,7 +232,7 @@ public enum Aggregator { primaryText = "\(totalTodayTokens)" } - // Menu bar shows one window (the user's preferred provider, else Claude 5h). + // Menu bar shows the selected provider's canonical window, with a cross-provider fallback. // quotaPercent is the *remaining* fraction (drives bar fill + color); the view // renders it as "used %". let quotaPercent: Double? = quota.menuWindow(preferring: menuQuotaProvider)?.remaining diff --git a/Sources/CodingBarCore/Models.swift b/Sources/CodingBarCore/Models.swift index 66fb6a7..85eed81 100644 --- a/Sources/CodingBarCore/Models.swift +++ b/Sources/CodingBarCore/Models.swift @@ -81,24 +81,25 @@ public extension Array where Element == QuotaWindow { /// The most-depleted window's remaining fraction, nil when there is no data. var tightestRemaining: Double? { map(\.remaining).min() } - /// The single window surfaced in the menu bar: Claude 5h preferred, else - /// Codex 5h, else the most-depleted window. Keeps the menu bar meaning one - /// fixed, predictable thing ("how much of my Claude 5h window is used"). + /// The single window surfaced in the menu bar: Claude 5h preferred, else Codex + /// 7d. Only these canonical limits are eligible because the menu bar has no label + /// that could honestly explain an arbitrary fallback window. var menuWindow: QuotaWindow? { first { $0.provider == .claude && $0.label == "5h" } - ?? first { $0.provider == .codex && $0.label == "5h" } - ?? self.min { $0.remaining < $1.remaining } + ?? first { $0.provider == .codex && $0.label == "7d" } } - /// Menu-bar window honoring the user's preferred provider: that provider's 5h - /// window, else its most-depleted window. Falls back to the default `menuWindow` - /// chain when the preferred provider has no data (so a Claude-only or Codex-only - /// user still sees something). `nil` preference → the default chain. + /// Menu-bar window honoring the user's preferred provider: Claude's 5h or Codex's + /// 7d window. If that canonical window is unavailable, falls back to the other + /// provider's canonical window; `nil` preference uses the default chain. func menuWindow(preferring provider: Provider?) -> QuotaWindow? { guard let provider else { return menuWindow } - let preferred = first { $0.provider == provider && $0.label == "5h" } - ?? filter { $0.provider == provider }.min { $0.remaining < $1.remaining } - return preferred ?? menuWindow + let label: String + switch provider { + case .claude: label = "5h" + case .codex: label = "7d" + } + return first { $0.provider == provider && $0.label == label } ?? menuWindow } } diff --git a/Tests/CodingBarCoreTests/SmokeTests.swift b/Tests/CodingBarCoreTests/SmokeTests.swift index 8f1d5b5..dcad640 100644 --- a/Tests/CodingBarCoreTests/SmokeTests.swift +++ b/Tests/CodingBarCoreTests/SmokeTests.swift @@ -535,6 +535,27 @@ final class SmokeTests: XCTestCase { XCTAssertEqual(unlabelable.map(\.label), ["7d"]) } + func testCodexMenuQuotaUsesWeeklyWindow() { + let windows = [ + QuotaWindow(provider: .codex, label: "5h", remaining: 0.91, resetAt: nil), + QuotaWindow(provider: .codex, label: "7d", remaining: 0.26, resetAt: nil), + ] + + XCTAssertEqual(windows.menuWindow(preferring: .codex)?.label, "7d") + } + + func testCodexMenuQuotaNeverFallsBackToFiveHourWindow() { + let codexFiveHourOnly = [ + QuotaWindow(provider: .codex, label: "5h", remaining: 0.91, resetAt: nil), + ] + XCTAssertNil(codexFiveHourOnly.menuWindow(preferring: .codex)) + + let withClaude = [ + QuotaWindow(provider: .claude, label: "5h", remaining: 0.72, resetAt: nil), + ] + codexFiveHourOnly + XCTAssertEqual(withClaude.menuWindow(preferring: .codex)?.provider, .claude) + } + /// A model-scoped weekly cap burns on its own curve — for a model priced above the plan /// average it usually empties well before the overall week — so it needs its own /// depletion line, keyed by window id rather than the shared provider key. diff --git a/release-notes/v1.1.8.md b/release-notes/v1.1.8.md new file mode 100644 index 0000000..ec2461a --- /dev/null +++ b/release-notes/v1.1.8.md @@ -0,0 +1,9 @@ +- The menu-bar quota provider setting now uses each provider's actual primary + limit: Claude shows the 5-hour window, while Codex shows the weekly window. + Selecting Codex previously still preferred a 5-hour-shaped window, so the + number could represent the wrong allowance. +- Codex no longer silently falls back to a 5-hour window when weekly data is + unavailable. CodingBar falls back to Claude's 5-hour limit when possible, or + leaves the quota readout hidden rather than displaying a misleading metric. + +**Full Changelog**: https://github.com/Gnonymous/CodingBar/compare/v1.1.7...v1.1.8