diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4052910..1ad9fa2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+## [1.5.1] - 2026-09-22
+
+### Fixed
+
+- Available Codex limit resets no longer break into a letter-by-letter column. Narrow cards show the full label on its own line below the update time, and wider cards keep it beside the time, with a tooltip explaining the count.
+
## [1.5.0] - 2026-09-20
### Added
diff --git a/Resources/Info.plist b/Resources/Info.plist
index f3ed138..fe15949 100644
--- a/Resources/Info.plist
+++ b/Resources/Info.plist
@@ -17,9 +17,9 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 1.5.0
+ 1.5.1
CFBundleVersion
- 7
+ 8
LSMinimumSystemVersion
14.0
LSUIElement
diff --git a/Sources/TokenGaugeApp/Resources/en.lproj/Localizable.strings b/Sources/TokenGaugeApp/Resources/en.lproj/Localizable.strings
index 7c6fc8d..7e5a756 100644
--- a/Sources/TokenGaugeApp/Resources/en.lproj/Localizable.strings
+++ b/Sources/TokenGaugeApp/Resources/en.lproj/Localizable.strings
@@ -26,6 +26,8 @@
"reset.tomorrow_at" = "Resets tomorrow · %@";
"reset_credits.one" = "1 reset";
"reset_credits.many" = "%d resets";
+"reset_credits.available_one" = "1 limit reset available";
+"reset_credits.available_many" = "%d limit resets available";
"updated.now" = "now";
"updated.never" = "no reading";
"model.unknown" = "Other";
diff --git a/Sources/TokenGaugeApp/Resources/es.lproj/Localizable.strings b/Sources/TokenGaugeApp/Resources/es.lproj/Localizable.strings
index 326d604..d3ffa92 100644
--- a/Sources/TokenGaugeApp/Resources/es.lproj/Localizable.strings
+++ b/Sources/TokenGaugeApp/Resources/es.lproj/Localizable.strings
@@ -26,6 +26,8 @@
"reset.tomorrow_at" = "Reinicia mañana · %@";
"reset_credits.one" = "1 reinicio";
"reset_credits.many" = "%d reinicios";
+"reset_credits.available_one" = "1 reinicio de límite disponible";
+"reset_credits.available_many" = "%d reinicios de límite disponibles";
"updated.now" = "ahora";
"updated.never" = "sin lectura";
"model.unknown" = "Otros";
diff --git a/Sources/TokenGaugeApp/Support/UsageFormatters.swift b/Sources/TokenGaugeApp/Support/UsageFormatters.swift
index 06f0022..5dde254 100644
--- a/Sources/TokenGaugeApp/Support/UsageFormatters.swift
+++ b/Sources/TokenGaugeApp/Support/UsageFormatters.swift
@@ -67,6 +67,10 @@ enum UsageFormatters {
count == 1 ? "reset_credits.one".localized : "reset_credits.many".localized(count)
}
+ @MainActor static func resetCreditsAvailable(_ count: Int) -> String {
+ count == 1 ? "reset_credits.available_one".localized : "reset_credits.available_many".localized(count)
+ }
+
@MainActor static func lastUpdated(_ date: Date?) -> String {
guard let date else { return "updated.never".localized }
let age = Date().timeIntervalSince(date)
diff --git a/Sources/TokenGaugeApp/Views/ProviderCard.swift b/Sources/TokenGaugeApp/Views/ProviderCard.swift
index 4cb0806..f5616d1 100644
--- a/Sources/TokenGaugeApp/Views/ProviderCard.swift
+++ b/Sources/TokenGaugeApp/Views/ProviderCard.swift
@@ -152,20 +152,40 @@ struct ProviderCard: View {
return "quota.remaining_value".localized(UsageFormatters.percentage(window.remainingPercentage))
}
+ @ViewBuilder
private var header: some View {
+ if state.status == .ready, let credits = state.snapshot?.availableResetCredits, credits > 0 {
+ ViewThatFits(in: .horizontal) {
+ headerRow(credits: credits, reservingStatus: widestFreshness)
+ VStack(alignment: .trailing, spacing: 6) {
+ headerRow(credits: nil, reservingStatus: nil)
+ ResetCreditsBadge(count: credits)
+ }
+ }
+ } else {
+ headerRow(credits: nil, reservingStatus: nil)
+ }
+ }
+
+ private var widestFreshness: String {
+ UsageFormatters.lastUpdated(Date().addingTimeInterval(-59 * 60))
+ }
+
+ private func headerRow(credits: Int?, reservingStatus reserved: String?) -> some View {
let showsTitle = showProviderTitle || panelStyle == .rings
return HStack(spacing: 5) {
if showsTitle {
ProviderLogo(provider: provider, size: 13)
- Text("provider.\(provider.rawValue)".localized).font(.system(size: 11, weight: .semibold))
+ Text("provider.\(provider.rawValue)".localized)
+ .font(.system(size: 11, weight: .semibold))
+ .lineLimit(1)
Spacer(minLength: 4)
}
- Circle().fill(statusColor).frame(width: 5, height: 5)
- Text(statusSubtitle)
- .font(.system(size: 10))
- .foregroundStyle(.secondary)
- .lineLimit(1)
- .layoutPriority(2)
+ ZStack(alignment: showsTitle ? .trailing : .leading) {
+ if let reserved { freshness(reserved).hidden() }
+ freshness(statusSubtitle)
+ }
+ .layoutPriority(2)
if provider == .claude, let account = accountLabel, !account.isEmpty {
Text(account)
.font(.system(size: 10))
@@ -176,14 +196,17 @@ struct ProviderCard: View {
.accessibilityLabel("account.current".localized(account))
}
if !showsTitle { Spacer(minLength: 4) }
- if state.status == .ready, let credits = state.snapshot?.availableResetCredits, credits > 0 {
- Text(UsageFormatters.resetCredits(credits))
- .font(.system(size: 10, weight: .medium))
- .foregroundStyle(.secondary)
- .padding(.horizontal, 6)
- .padding(.vertical, 2)
- .background(Capsule().fill(Color.primary.opacity(0.05)))
- }
+ if let credits { ResetCreditsBadge(count: credits) }
+ }
+ }
+
+ private func freshness(_ text: String) -> some View {
+ HStack(spacing: 5) {
+ Circle().fill(statusColor).frame(width: 5, height: 5)
+ Text(text)
+ .font(.system(size: 10))
+ .foregroundStyle(.secondary)
+ .lineLimit(1)
}
}
diff --git a/Sources/TokenGaugeApp/Views/ResetCreditsBadge.swift b/Sources/TokenGaugeApp/Views/ResetCreditsBadge.swift
new file mode 100644
index 0000000..169ad1d
--- /dev/null
+++ b/Sources/TokenGaugeApp/Views/ResetCreditsBadge.swift
@@ -0,0 +1,17 @@
+import SwiftUI
+
+struct ResetCreditsBadge: View {
+ let count: Int
+
+ var body: some View {
+ Text(UsageFormatters.resetCredits(count))
+ .font(.system(size: 10, weight: .medium).monospacedDigit())
+ .foregroundStyle(.secondary)
+ .lineLimit(1)
+ .fixedSize()
+ .padding(.horizontal, 6)
+ .background(Capsule().fill(Color.primary.opacity(0.05)).padding(.vertical, -2))
+ .help(UsageFormatters.resetCreditsAvailable(count))
+ .accessibilityLabel(UsageFormatters.resetCreditsAvailable(count))
+ }
+}
diff --git a/Tests/TokenGaugeAppTests/ResetCreditsBadgeRenderingTests.swift b/Tests/TokenGaugeAppTests/ResetCreditsBadgeRenderingTests.swift
new file mode 100644
index 0000000..8583025
--- /dev/null
+++ b/Tests/TokenGaugeAppTests/ResetCreditsBadgeRenderingTests.swift
@@ -0,0 +1,61 @@
+import AppKit
+import SwiftUI
+import TokenGaugeCore
+import XCTest
+
+@testable import TokenGaugeApp
+
+@MainActor
+final class ResetCreditsBadgeRenderingTests: XCTestCase {
+ private let narrowRingCard = Theme.Layout.minimumRingCardWidth
+
+ func testWideCardsKeepResetCreditsOnTheHeaderRow() throws {
+ try withEachLanguage { language in
+ for style in [QuotaPanelStyle.rings, .standard] {
+ let plain = fittingSize(card(credits: nil, width: Theme.Layout.compactPanelWidth, age: 0, style: style))
+ for age: TimeInterval in [0, -720, -3_540] {
+ for credits in [1, 3, 12, 120] {
+ let size = fittingSize(
+ card(credits: credits, width: Theme.Layout.compactPanelWidth, age: age, style: style))
+ XCTAssertEqual(size.height, plain.height, accuracy: 1, "\(language) \(style) \(age) \(credits)")
+ }
+ }
+ }
+ }
+ }
+
+ func testNarrowCardsMoveResetCreditsToOneStableRowWhateverTheFreshness() throws {
+ try withEachLanguage { language in
+ for style in [QuotaPanelStyle.rings, .standard] {
+ let plain = fittingSize(card(credits: nil, width: narrowRingCard, age: 0, style: style))
+ let reference = fittingSize(card(credits: 1, width: narrowRingCard, age: 0, style: style))
+ XCTAssertGreaterThan(reference.height, plain.height + 10, "\(language) \(style)")
+ XCTAssertLessThan(reference.height, plain.height + 30, "\(language) \(style)")
+ for age: TimeInterval in [0, -720, -3_540] {
+ for credits in [1, 3, 12, 120] {
+ let size = fittingSize(card(credits: credits, width: narrowRingCard, age: age, style: style))
+ XCTAssertEqual(
+ size.height, reference.height, accuracy: 1, "\(language) \(style) \(age) \(credits)")
+ }
+ }
+ }
+ }
+ }
+
+ private func card(credits: Int?, width: CGFloat, age: TimeInterval, style: QuotaPanelStyle) -> some View {
+ ProviderCard(
+ provider: .codex,
+ state: ProviderViewState(
+ snapshot: Fixture.snapshot(
+ .codex,
+ windows: [
+ Fixture.window(
+ id: "codex.secondary", usedPercentage: 90, resetsAt: Date().addingTimeInterval(432_000),
+ durationMinutes: 10_080)
+ ], availableResetCredits: credits, capturedAt: Date().addingTimeInterval(age)),
+ status: .ready, isRefreshing: false),
+ panelStyle: style, showProviderTitle: true
+ )
+ .frame(width: width)
+ }
+}