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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ on Columbus Labs QuotaKit releases and product-facing changes.

### Added

- Providers: add Codex personal-access-token usage, OpenCode Go API quotas, Fireworks account discovery, Kiro overage limits, z.ai China balances, and live Grok/xAI spend sources.
- Menu bar: add reusable conditional layout rules, richer conditional metrics, and stable multi-line rendering.
- Claude: retain complete claude-swap usage at exhausted limits and disambiguate same-email accounts with stable aliases.
- Personalization: add per-provider accent colors, configurable workday tick contrast, removable widget backgrounds, compact run-out tokens, and token-aware cost history charts.
- Usage & Spend: add source-safe project breakdowns, partial-cost reporting, provider-qualified model pricing, and Codex session grouping in the CLI.
- Accounts: add bounded Kiro reauthentication, Cursor app-session discovery, explicit Codex external-auth controls, and settings recovery for minimized windows.
Expand All @@ -25,6 +28,8 @@ on Columbus Labs QuotaKit releases and product-facing changes.

### Fixed

- Usage & Spend: share one provider-source publication between Overview and the dashboard, count every enabled provider, align calendar bucketing, and drain completed Codex catch-up files.
- Mac reliability: preserve cached menu-card subclasses, prevent post-exit RPC pipe aborts, and size agent-session menus from their actual content.
- Codex: keep provider-owned OAuth files read-only, recover account identity from JWT claims, preserve established-empty history, and retain known spend when some models cannot be priced.
- Claude, Cursor, Grok, Ollama, OpenCode Go, Vertex AI, and Antigravity: improve account recovery, local-source authority, plan labels, cookie parsing, quota estimates, limit matching, and dashboard lane selection without live credential probing.
- Mac lifecycle: remove the hidden keepalive window while retaining continuous Mac-to-iPhone sync and noninteractive legacy Keychain migration from durable app startup ownership.
Expand Down
122 changes: 96 additions & 26 deletions CodexBarMobile/CodexBarMobile/Views/KiroCreditsCard.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,38 @@
import CodexBarSync
import SwiftUI

struct KiroOveragePresentation: Equatable {
let creditsUsed: Double?
let creditsCap: Double?
let creditsRemaining: Double?
let creditsFraction: Double?
let charges: Double?
let chargeLimit: Double?
let currencyCode: String

init?(_ credits: SyncKiroCredits) {
let used = credits.overageCreditsUsed
let cap = credits.overageCreditsCap.flatMap { $0 > 0 ? $0 : nil }
let charges = credits.overageCharges ?? credits.estimatedOverageCostUSD
guard (used ?? 0) > 0 || cap != nil || (charges ?? 0) > 0 else { return nil }

self.creditsUsed = used
self.creditsCap = cap
self.creditsRemaining = cap.map { max(0, $0 - (used ?? 0)) }
self.creditsFraction = cap.map { min(max((used ?? 0) / $0, 0), 1) }
self.charges = charges
self.chargeLimit = credits.overageChargeLimit
self.currencyCode = credits.overageCharges != nil
? Self.normalizedCurrencyCode(credits.overageCurrencyCode)
: "USD"
}

private static func normalizedCurrencyCode(_ code: String?) -> String {
let normalized = code?.trimmingCharacters(in: .whitespacesAndNewlines).uppercased() ?? ""
return normalized.count == 3 ? normalized : "USD"
}
}

/// Dedicated Kiro credit-display card. Mirrors the Mac MenuCard
/// affordance added in upstream PR #933 — plan tag + primary credits
/// progress + optional bonus pool with expiry countdown.
Expand Down Expand Up @@ -29,9 +61,8 @@ struct KiroCreditsCard: View {
self.credits.bonusTotal != nil && (self.credits.bonusTotal ?? 0) > 0
}

private var hasOverage: Bool {
(self.credits.overageCreditsUsed ?? 0) > 0
|| (self.credits.estimatedOverageCostUSD ?? 0) > 0
private var overagePresentation: KiroOveragePresentation? {
KiroOveragePresentation(self.credits)
}

var body: some View {
Expand All @@ -45,9 +76,9 @@ struct KiroCreditsCard: View {
self.bonusRow(fraction: bonusFraction)
}

if self.hasOverage {
if let overage = self.overagePresentation {
Divider()
self.overageRow
self.overageRow(overage)
}
}
.padding(16)
Expand All @@ -60,34 +91,69 @@ struct KiroCreditsCard: View {
/// `overage_credits_used` (Kiro plan exhausted, user paying
/// per-credit). Mirrors Mac's v0.27.0 "overage credits / overage
/// cost" menu bar display modes.
private var overageRow: some View {
HStack(alignment: .firstTextBaseline, spacing: 8) {
Text(String(localized: "kiro_overage_label", defaultValue: "Overage"))
.font(.caption)
.foregroundStyle(.secondary)
Spacer()
if let usedCredits = credits.overageCreditsUsed, usedCredits > 0 {
Text(String(
format: String(localized: "kiro_overage_credits_format", defaultValue: "+%@ credits"),
Self.formatCredits(usedCredits)))
.font(.caption.bold().monospacedDigit())
.foregroundStyle(.orange)
private func overageRow(_ overage: KiroOveragePresentation) -> some View {
VStack(alignment: .leading, spacing: 6) {
HStack(alignment: .firstTextBaseline, spacing: 8) {
Text(String(localized: "kiro_overage_label", defaultValue: "Overage"))
.font(.caption)
.foregroundStyle(.secondary)
Spacer()
if let usedCredits = overage.creditsUsed {
Text(self.overageCreditsText(used: usedCredits, cap: overage.creditsCap))
.font(.caption.bold().monospacedDigit())
.foregroundStyle(.orange)
}
}
if let costUSD = credits.estimatedOverageCostUSD, costUSD > 0 {
Text(self.overageCostText(costUSD: costUSD))
.font(.caption.bold().monospacedDigit())
.foregroundStyle(.orange)

if let fraction = overage.creditsFraction {
ProgressView(value: fraction)
.progressViewStyle(.linear)
.tint(.orange)
}

HStack(alignment: .firstTextBaseline, spacing: 8) {
if let remaining = overage.creditsRemaining {
Text(String(
format: String(localized: "%@ credits left"),
Self.formatCredits(remaining)))
.font(.caption.monospacedDigit())
.foregroundStyle(.secondary)
}
Spacer()
if let charges = overage.charges {
Text(self.overageCostText(
charges: charges,
limit: overage.chargeLimit,
currencyCode: overage.currencyCode))
.font(.caption.bold().monospacedDigit())
.foregroundStyle(.orange)
}
}
}
.accessibilityIdentifier("kiro-overage-row")
}

private func overageCostText(costUSD: Double) -> String {
private func overageCreditsText(used: Double, cap: Double?) -> String {
guard let cap else {
return String(
format: String(localized: "kiro_overage_credits_format", defaultValue: "+%@ credits"),
Self.formatCredits(used))
}
return "\(Self.formatCredits(used)) / \(Self.formatCredits(cap))"
}

private func overageCostText(charges: Double, limit: Double?, currencyCode: String) -> String {
let used = Self.currencyText(charges, currencyCode: currencyCode)
guard let limit else { return used }
return "\(used) / \(Self.currencyText(limit, currencyCode: currencyCode))"
}

private static func currencyText(_ value: Double, currencyCode: String) -> String {
let formatter = NumberFormatter()
formatter.numberStyle = .currency
formatter.currencyCode = "USD"
formatter.maximumFractionDigits = costUSD < 10 ? 2 : 0
return formatter.string(from: NSNumber(value: costUSD)) ?? "$\(costUSD)"
formatter.currencyCode = currencyCode
formatter.maximumFractionDigits = value < 10 ? 2 : 0
return formatter.string(from: NSNumber(value: value)) ?? "\(currencyCode) \(value)"
}

private var header: some View {
Expand Down Expand Up @@ -218,7 +284,11 @@ struct KiroCreditsCard: View {
bonusExpiryDays: 7,
resetsAt: nil,
overageCreditsUsed: 145,
estimatedOverageCostUSD: 2.45),
estimatedOverageCostUSD: nil,
overageCreditsCap: 500,
overageCharges: 18.85,
overageChargeLimit: 65,
overageCurrencyCode: "EUR"),
tintColor: Color(red: 0.25, green: 0.62, blue: 0.49))
.padding()
}
47 changes: 47 additions & 0 deletions CodexBarMobile/CodexBarMobileTests/V026ViewSmokeTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,53 @@ final class V026ViewSmokeTests: XCTestCase {
XCTAssertNotNil(image)
}

func testKiroOveragePresentationUsesCapRemainingAndNativeCurrency() throws {
let credits = SyncKiroCredits(
planName: "Pro",
creditsUsed: 1000,
creditsTotal: 1000,
creditsPercent: 100,
bonusUsed: nil,
bonusTotal: nil,
bonusExpiryDays: nil,
resetsAt: Date(),
overageCreditsUsed: 125,
estimatedOverageCostUSD: nil,
overageCreditsCap: 500,
overageCharges: 18.75,
overageChargeLimit: 75,
overageCurrencyCode: "eur")

let presentation = try XCTUnwrap(KiroOveragePresentation(credits))

XCTAssertEqual(presentation.creditsRemaining, 375)
XCTAssertEqual(presentation.creditsFraction, 0.25)
XCTAssertEqual(presentation.charges, 18.75)
XCTAssertEqual(presentation.chargeLimit, 75)
XCTAssertEqual(presentation.currencyCode, "EUR")
}

func testKiroOveragePresentationFallsBackToLegacyUSDEstimate() throws {
let credits = SyncKiroCredits(
planName: "Pro",
creditsUsed: 1000,
creditsTotal: 1000,
creditsPercent: 100,
bonusUsed: nil,
bonusTotal: nil,
bonusExpiryDays: nil,
resetsAt: nil,
overageCreditsUsed: 25,
estimatedOverageCostUSD: 2.5)

let presentation = try XCTUnwrap(KiroOveragePresentation(credits))

XCTAssertEqual(presentation.charges, 2.5)
XCTAssertEqual(presentation.currencyCode, "USD")
XCTAssertNil(presentation.creditsCap)
XCTAssertNil(presentation.creditsRemaining)
}

func testAntigravityAccountSwitcherRendersSingleAccount() {
// When only one Google account is wired, the switcher should
// still render the row (caller already gates count > 1 in the
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ Provider setup notes and Mac provider internals live in [docs/providers.md](docs
- [Claude](docs/claude.md) — OAuth API, browser cookies, or CLI PTY fallback; session and weekly usage where available.
- [Cursor](docs/cursor.md) — Browser session cookies for plan + usage + billing resets.
- [OpenCode](docs/opencode.md) — Browser cookies for workspace subscription usage.
- [OpenCode Go](docs/opencode.md) — Browser or local SQLite data for Go usage windows.
- [OpenCode Go](docs/opencode.md) — Usage API, browser fallback, and local SQLite cost history.
- [Alibaba Coding Plan](docs/alibaba-coding-plan.md) — Web cookies or API key for coding-plan quotas.
- [Alibaba Token Plan](docs/alibaba-token-plan.md) — Bailian browser/manual cookies for token-plan credits.
- [Qwen Cloud](docs/qwen-cloud.md) — 5-hour and weekly individual Token Plan usage via browser/manual cookies.
Expand Down
30 changes: 24 additions & 6 deletions Shared/Models/V026Snapshots.swift
Original file line number Diff line number Diff line change
Expand Up @@ -175,10 +175,8 @@ public struct SyncZaiHourlyUsage: Codable, Sendable, Equatable {
/// Kiro plan + monthly credit allowance + optional bonus pool.
/// Populated only on the `kiro` provider snapshot.
///
/// v0.27.0 (upstream) added two overage fields — `overageCreditsUsed`
/// and `estimatedOverageCostUSD` — that Mac surfaces when a plan has
/// been exhausted. Both are optional + decoded with `decodeIfPresent`
/// so pre-v0.27.0 payloads (no overage data) still decode cleanly.
/// Overage fields are optional and decoded with `decodeIfPresent` so older
/// payloads and clients remain compatible as the Mac learns richer limits.
public struct SyncKiroCredits: Codable, Sendable, Equatable {
public let planName: String?
public let creditsUsed: Double
Expand All @@ -194,8 +192,16 @@ public struct SyncKiroCredits: Codable, Sendable, Equatable {
public let overageCreditsUsed: Double?
/// Mac-computed `(overageCreditsUsed * priceUSD)` estimate. Always
/// USD; nil when no overage data is present or when Kiro has not
/// surfaced a price. iOS displays this as a "overage cost" badge.
/// surfaced a price. Retained for older Mac payloads and iOS clients.
public let estimatedOverageCostUSD: Double?
/// Maximum overage credits the account can spend.
public let overageCreditsCap: Double?
/// Actual accrued overage charge in `overageCurrencyCode`.
public let overageCharges: Double?
/// Monetary ceiling corresponding to `overageCreditsCap`.
public let overageChargeLimit: Double?
/// ISO 4217 currency code for `overageCharges` and `overageChargeLimit`.
public let overageCurrencyCode: String?

public init(
planName: String?,
Expand All @@ -207,7 +213,11 @@ public struct SyncKiroCredits: Codable, Sendable, Equatable {
bonusExpiryDays: Int?,
resetsAt: Date?,
overageCreditsUsed: Double? = nil,
estimatedOverageCostUSD: Double? = nil)
estimatedOverageCostUSD: Double? = nil,
overageCreditsCap: Double? = nil,
overageCharges: Double? = nil,
overageChargeLimit: Double? = nil,
overageCurrencyCode: String? = nil)
{
self.planName = planName
self.creditsUsed = creditsUsed
Expand All @@ -219,6 +229,10 @@ public struct SyncKiroCredits: Codable, Sendable, Equatable {
self.resetsAt = resetsAt
self.overageCreditsUsed = overageCreditsUsed
self.estimatedOverageCostUSD = estimatedOverageCostUSD
self.overageCreditsCap = overageCreditsCap
self.overageCharges = overageCharges
self.overageChargeLimit = overageChargeLimit
self.overageCurrencyCode = overageCurrencyCode
}

public init(from decoder: Decoder) throws {
Expand All @@ -234,6 +248,10 @@ public struct SyncKiroCredits: Codable, Sendable, Equatable {
// v0.27.0 additions — decodeIfPresent so v0.26 payloads decode.
self.overageCreditsUsed = try c.decodeIfPresent(Double.self, forKey: .overageCreditsUsed)
self.estimatedOverageCostUSD = try c.decodeIfPresent(Double.self, forKey: .estimatedOverageCostUSD)
self.overageCreditsCap = try c.decodeIfPresent(Double.self, forKey: .overageCreditsCap)
self.overageCharges = try c.decodeIfPresent(Double.self, forKey: .overageCharges)
self.overageChargeLimit = try c.decodeIfPresent(Double.self, forKey: .overageChargeLimit)
self.overageCurrencyCode = try c.decodeIfPresent(String.self, forKey: .overageCurrencyCode)
}
}

Expand Down
16 changes: 16 additions & 0 deletions Sources/CodexBar/CodexbarApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
self.installDebugMemoryPressureObserverIfNeeded()
#endif
self.ensureStatusController()
self.closeSwiftUISettingsPlaceholderWindow()
self.observeSettingsApplicationMenuLanguage()
self.scheduleSettingsApplicationMenuValidation(
missingItemRetriesRemaining: Self.settingsMenuReadinessRetryCount,
Expand Down Expand Up @@ -523,6 +524,21 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
}
}

/// The SwiftUI `Settings` scene exists only to own the app-menu Settings command; the real
/// settings window is AppKit-managed (`SettingsWindowController`). macOS can still present or
/// state-restore the scene's empty placeholder window at launch — close it and keep it out of
/// state restoration so it cannot come back on the next launch.
private func closeSwiftUISettingsPlaceholderWindow() {
DispatchQueue.main.async {
for window in NSApp.windows
where window.identifier?.rawValue.hasPrefix("com_apple_SwiftUI_Settings") == true
{
window.isRestorable = false
window.close()
}
}
}

func applicationWillTerminate(_ notification: Notification) {
self.cloudSyncCoordinator?.stop()
self.memoryPressureMonitor.stop()
Expand Down
18 changes: 18 additions & 0 deletions Sources/CodexBar/Localization.swift
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,24 @@ func L(_ key: String, language: String) -> String {
return codexBarLocalizedString(key, bundle: bundle, resourceBundle: resourceBundle)
}

/// Uses an explicit duration for Simplified Chinese quota surfaces while preserving the generic
/// `Session` translation for conversations and other non-quota UI.
func localizedSessionQuotaLabel(_ label: String, windowMinutes: Int?) -> String {
let localizedLabel = L(label)
guard label == "Session",
localizedBundle().bundleURL.lastPathComponent.caseInsensitiveCompare("zh-Hans.lproj") == .orderedSame,
let windowMinutes
else { return localizedLabel }

if windowMinutes == 7 * 24 * 60 {
return L("Weekly")
}
guard (60...(12 * 60)).contains(windowMinutes), windowMinutes.isMultiple(of: 60) else {
return localizedLabel
}
return "\(codexBarLocalizedInteger(windowMinutes / 60)) \(L("Hour"))"
}

func codexBarLocalizedLocale() -> Locale {
codexBarLocale(forLanguage: resolvedAppLanguage())
}
Expand Down
Loading