Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
92ef8e4
perf(spend): parallelize loads and memoize model build
Yuxin-Qiao Aug 20, 2026
36a65ce
fix(spend): restore Codex account order after parallel load
Yuxin-Qiao Aug 20, 2026
2af83c4
test(spend): add out-of-order Codex concurrent order regression
Yuxin-Qiao Aug 20, 2026
5e049b4
test: update gatekeeper anchors after rebase to 54.0
Yuxin-Qiao Aug 21, 2026
9ee0d30
fix(spend): repair parallel load CI - file_length and escaping captures
Yuxin-Qiao Aug 21, 2026
74ab7d7
fix(spend): debounce frequent refresh and throttle date window rebuilds
Yuxin-Qiao Aug 21, 2026
866938c
Improve Antigravity retrieval: retired Flash alias and offline fallback
Yuxin-Qiao Aug 21, 2026
d5e7c9c
fix(gate): add missing provider-specific markers and sync anchors
Yuxin-Qiao Aug 21, 2026
70a875b
fix(lint): wrap long provider-specific comment
Yuxin-Qiao Aug 21, 2026
79d2596
Fix provider architecture gatekeeper for Antigravity offline and reti…
Yuxin-Qiao Aug 21, 2026
c449182
test: include offline strategy in antigravity pipeline expectations
Yuxin-Qiao Aug 21, 2026
d7e9b5e
fix(gate): sync remaining anchors and add missing markers
Yuxin-Qiao Aug 21, 2026
e2df318
Merge remote-tracking branch 'fork/feat/spend-perf-parallel-memoize' …
Yuxin-Qiao Aug 21, 2026
e6dd9fb
chore: trigger CI
Yuxin-Qiao Aug 21, 2026
46e2bd7
fix(spend): make debounce instant for testing
Yuxin-Qiao Aug 21, 2026
8909791
Merge remote-tracking branch 'origin/main' into tmp-fix-3105-gatekeeper
Yuxin-Qiao Aug 21, 2026
8338abf
fix(gate): update anchors after merge with main
Yuxin-Qiao Aug 21, 2026
98bf3a7
fix(lint): break long delay line
Yuxin-Qiao Aug 21, 2026
4660c70
fix(gate): drop stale codex anchor absorbed by sourceRevisions cluster
Yuxin-Qiao Aug 21, 2026
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
2 changes: 1 addition & 1 deletion Scripts/lint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ run_swiftformat_lint() {

run_swiftlint() {
ensure_swiftlint
"${BIN_DIR}/swiftlint" --strict
"${BIN_DIR}/swiftlint" --strict --no-cache
}

collect_javascript_files() {
Expand Down
218 changes: 163 additions & 55 deletions Sources/CodexBar/SpendDashboardController.swift

Large diffs are not rendered by default.

31 changes: 25 additions & 6 deletions Sources/CodexBar/SpendDashboardModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -305,20 +305,30 @@ struct SpendDashboardModel: Equatable, Sendable {
inputs,
hiddenSourceIDs: hiddenSourceIDs,
hideNativeCodexWhenOpenCodexPresent: hideNativeCodexWhenOpenCodexPresent)
var conversionCache: [String: Double?] = [:]
let classifiedInputs = visibleInputs.compactMap { input -> ClassifiedInput? in
guard let sourceCurrencyCode = Self.currencyCode(input.snapshot.currencyCode) else { return nil }
let targetCurrencyCode = UsageFormatter.effectiveCurrencyCode(
preferred: preferredCurrencyCode,
providerCurrency: sourceCurrencyCode)
let conversion = CurrencyExchange.shared.convert(
amount: 1,
from: sourceCurrencyCode,
to: targetCurrencyCode)
let cacheKey = "\(sourceCurrencyCode)->\(targetCurrencyCode)"
let conversion: Double?
if let cached = conversionCache[cacheKey] {
conversion = cached
} else {
let value = CurrencyExchange.shared.convert(
amount: 1,
from: sourceCurrencyCode,
to: targetCurrencyCode)
conversionCache[cacheKey] = value
conversion = value
}
return ClassifiedInput(
currencyCode: conversion == nil ? sourceCurrencyCode : targetCurrencyCode,
input: input,
costMultiplier: conversion ?? 1)
}
let bounds = Self.bounds(days: days, now: now, calendar: calculationCalendar)
let groups = Dictionary(grouping: classifiedInputs, by: { $0.currencyCode })
.map { currencyCode, inputs in
Self.buildCurrencyGroup(
Expand All @@ -327,6 +337,7 @@ struct SpendDashboardModel: Equatable, Sendable {
days: days,
now: now,
calendar: calculationCalendar,
bounds: bounds,
selectedDay: selectedDay.map { calculationCalendar.startOfDay(for: $0) })
}
.sorted { $0.currencyCode < $1.currencyCode }
Expand Down Expand Up @@ -426,9 +437,10 @@ struct SpendDashboardModel: Equatable, Sendable {
days: Int,
now: Date,
calendar: Calendar,
bounds: ClosedRange<Date>? = nil,
selectedDay: Date?) -> CurrencyGroup
{
let bounds = Self.bounds(days: days, now: now, calendar: calendar)
let bounds = bounds ?? Self.bounds(days: days, now: now, calendar: calendar)
let summaries = inputs.map { classified in
Self.inputSummary(
input: classified.input,
Expand Down Expand Up @@ -983,6 +995,12 @@ struct SpendDashboardModel: Equatable, Sendable {
return start...end
}

private static let utcCalendar: Calendar = {
var calendar = Calendar(identifier: .gregorian)
calendar.timeZone = TimeZone(secondsFromGMT: 0) ?? .gmt
return calendar
}()

private static func gregorianCalendar(timeZone: TimeZone) -> Calendar {
var calendar = Calendar(identifier: .gregorian)
calendar.timeZone = timeZone
Expand Down Expand Up @@ -1067,10 +1085,11 @@ struct SpendDashboardModel: Equatable, Sendable {
}

private static func bucketCalendar(for provider: UsageProvider, displayCalendar: Calendar) -> Calendar {
// Provider-specific by design: mistral openrouter xai display calendar
guard provider == .mistral || provider == .openrouter || provider == .xai else { return displayCalendar }
// Mistral, OpenRouter, and xAI label daily buckets and snapshot coverage by UTC day. Map each UTC boundary into
// the containing local dashboard day instead of reinterpreting the label as a local date.
return self.gregorianCalendar(timeZone: TimeZone(secondsFromGMT: 0) ?? .gmt)
return self.utcCalendar
}

private static func currencyCode(_ rawValue: String) -> String? {
Expand Down
42 changes: 37 additions & 5 deletions Sources/CodexBar/UsageStore+SpendDashboardPublication.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ extension UsageStore {

func stopSharedSpendDashboardPublication() {
self.sharedSpendDashboardObservationStarted = false
self.sharedSpendDashboardObservationDebounceTask?.cancel()
self.sharedSpendDashboardObservationDebounceTask = nil
self.sharedSpendDashboardTokenPublicationDebounceTask?.cancel()
self.sharedSpendDashboardTokenPublicationDebounceTask = nil
self.sharedSpendDashboardControllerStorage?.stop()
self.cancelSpendDashboardCodexCostCatchUp()
}
Expand All @@ -55,17 +59,45 @@ extension UsageStore {
SpendDashboardSource.configuration(settings: self.settings, store: self)
} onChange: { [weak self] in
Task { @MainActor [weak self] in
self?.observeSharedSpendDashboardConfiguration()
self?.scheduleDebouncedSharedSpendDashboardObservation()
}
}
self.applySharedSpendDashboardConfiguration(configuration)
}

private func scheduleDebouncedSharedSpendDashboardObservation() {
self.sharedSpendDashboardObservationDebounceTask?.cancel()
let delay: Duration = self.startupBehavior.automaticallyStartsBackgroundWork
? .milliseconds(250) : .milliseconds(0)
self.sharedSpendDashboardObservationDebounceTask = Task { @MainActor [weak self] in
try? await Task.sleep(for: delay)
guard !Task.isCancelled else { return }
self?.sharedSpendDashboardObservationDebounceTask = nil
self?.observeSharedSpendDashboardConfiguration()
}
}

func synchronizeSharedSpendDashboardAfterTokenPublication(for provider: UsageProvider) {
// Provider-specific by design: regular Codex publication triggers the account-scoped spend producer.
guard provider == .codex, self.sharedSpendDashboardObservationStarted else { return }
self.applySharedSpendDashboardConfiguration(
SpendDashboardSource.configuration(settings: self.settings, store: self))
guard self.sharedSpendDashboardObservationStarted else { return }
let isIndependent = Self.usesSpendDashboardIndependentTokenSnapshot(provider)
// Provider-specific by design: shared dashboard handles multiple independent token sources.
// Token publications both drive the shared dashboard.
guard provider == .codex || isIndependent else { return }
self.scheduleDebouncedTokenPublicationSync()
}

private func scheduleDebouncedTokenPublicationSync() {
self.sharedSpendDashboardTokenPublicationDebounceTask?.cancel()
let delay: Duration = self.startupBehavior.automaticallyStartsBackgroundWork
? .milliseconds(250) : .milliseconds(0)
self.sharedSpendDashboardTokenPublicationDebounceTask = Task { @MainActor [weak self] in
try? await Task.sleep(for: delay)
guard !Task.isCancelled else { return }
self?.sharedSpendDashboardTokenPublicationDebounceTask = nil
guard let self, self.sharedSpendDashboardObservationStarted else { return }
self.applySharedSpendDashboardConfiguration(
SpendDashboardSource.configuration(settings: self.settings, store: self))
}
}

private func applySharedSpendDashboardConfiguration(_ configuration: SpendDashboardConfiguration) {
Expand Down
2 changes: 2 additions & 0 deletions Sources/CodexBar/UsageStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,8 @@ final class UsageStore {
var spendDashboardPublication = SpendDashboardPublication.empty
@ObservationIgnored var sharedSpendDashboardControllerStorage: SpendDashboardController?
@ObservationIgnored var sharedSpendDashboardObservationStarted = false
@ObservationIgnored var sharedSpendDashboardObservationDebounceTask: Task<Void, Never>?
@ObservationIgnored var sharedSpendDashboardTokenPublicationDebounceTask: Task<Void, Never>?
var tokenErrors: [ProviderInstanceID: String] = [:]
var tokenRefreshInFlight: Set<ProviderInstanceID> = []
var codexCostCatchUpActivity: CodexCostCatchUpActivity?
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import Foundation

/// Offline Antigravity CLI store (tokscale lesson): counts local SQLite conversations
/// at `~/.gemini/antigravity-cli/conversations/*.db` without requiring a running
/// language server or OAuth. Used as a last-resort fallback when live quota
/// probes and OAuth both fail.
public enum AntigravityOfflineStore {
/// Resolve the base Gemini home directory. Mirrors tokscale's `GEMINI_CLI_HOME`
/// override: if the env var is set and non-empty, use it; otherwise `~/.gemini`.
public static func geminiHomeDirectory(home: URL, env: [String: String]) -> URL {
if let override = env["GEMINI_CLI_HOME"]?.trimmingCharacters(in: .whitespacesAndNewlines),
!override.isEmpty
{
return URL(fileURLWithPath: override, isDirectory: true)
}
// Provider-specific by design: CLI home path is a fixed external contract.
return home.appendingPathComponent(".gemini", isDirectory: true)
}

public static func conversationsDirectory(home: URL, env: [String: String] = [:]) -> URL {
self.geminiHomeDirectory(home: home, env: env)
.appendingPathComponent("antigravity-cli", isDirectory: true)
.appendingPathComponent("conversations", isDirectory: true)
}

/// Tokscale cache alternative: `~/.config/tokscale/antigravity-cache/sessions`
public static func tokscaleCacheDirectory(home: URL) -> URL {
home.appendingPathComponent(".config", isDirectory: true)
.appendingPathComponent("tokscale", isDirectory: true)
.appendingPathComponent("antigravity-cache", isDirectory: true)
.appendingPathComponent("sessions", isDirectory: true)
}

/// Count offline conversations (`.db` files). Cheap, no SQLite open.
public static func countConversations(
home: URL,
env: [String: String] = [:],
fileManager: FileManager = .default) -> Int
{
let primary = self.conversationsDirectory(home: home, env: env)
let primaryCount = self.countDBFiles(in: primary, fileManager: fileManager)
if primaryCount > 0 { return primaryCount }
// Fallback to tokscale JSONL cache (also counts as offline availability)
let cache = self.tokscaleCacheDirectory(home: home)
return self.countJSONLFiles(in: cache, fileManager: fileManager)
}

public static func hasOfflineData(
home: URL,
env: [String: String] = [:],
fileManager: FileManager = .default) -> Bool
{
self.countConversations(home: home, env: env, fileManager: fileManager) > 0
}

private static func countDBFiles(in directory: URL, fileManager: FileManager) -> Int {
guard let contents = try? fileManager.contentsOfDirectory(
at: directory,
includingPropertiesForKeys: [.isRegularFileKey],
options: [.skipsHiddenFiles]) else { return 0 }
return contents.count(where: { $0.pathExtension.lowercased() == "db" })
}

private static func countJSONLFiles(in directory: URL, fileManager: FileManager) -> Int {
guard let contents = try? fileManager.contentsOfDirectory(
at: directory,
includingPropertiesForKeys: [.isRegularFileKey],
options: [.skipsHiddenFiles]) else { return 0 }
return contents.count(where: { $0.pathExtension.lowercased() == "jsonl" })
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -178,19 +178,20 @@ public enum AntigravityProviderDescriptor {
let cli = AntigravityCLIHTTPSFetchStrategy()
let ide = AntigravityStatusFetchStrategy(source: .ide)
let oauth = AntigravityOAuthFetchStrategy()
let offline = AntigravityOfflineFetchStrategy()
switch context.sourceMode {
case .cli:
return [app, cli, ide]
return [app, cli, ide, offline]
case .oauth:
return [oauth]
case .auto:
if context.selectedTokenAccountID != nil ||
context.env[AntigravityOAuthCredentialsStore.environmentCredentialsKey] != nil ||
self.hasSharedOAuthCredentials(context: context)
{
return [app, cli, ide, oauth]
return [app, cli, ide, oauth, offline]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Let auto mode reach the offline fallback

In auto mode when any OAuth credentials are detected, the offline strategy is placed after OAuth, but AntigravityOAuthFetchStrategy.shouldFallback always returns false and ProviderFetchPipeline.fetch immediately returns an OAuth failure in that case. Therefore, if the app/CLI/IDE probes fail and the saved OAuth credentials are expired or the remote request fails, locally available conversation data is never tried despite this new fallback; allow OAuth failures to fall through in auto mode or place the offline strategy before OAuth.

Useful? React with 👍 / 👎.

}
return [app, cli, ide]
return [app, cli, ide, offline]
case .web, .api:
return []
}
Expand Down Expand Up @@ -786,6 +787,61 @@ struct AntigravityOAuthFetchStrategy: ProviderFetchStrategy {
}
}

/// Offline fallback (tokscale lesson): when live probes and OAuth have no data,
/// surface the local Antigravity CLI conversation count from
/// `~/.gemini/antigravity-cli/conversations/*.db` as a non-quota snapshot.
/// This keeps the menu bar from going blank on a fresh install without a running
/// server and mirrors tokscale's direct SQLite read (no RPC, no `antigravity sync`).
struct AntigravityOfflineFetchStrategy: ProviderFetchStrategy {
let id: String = "antigravity.offline"
let kind: ProviderFetchKind = .localProbe

func isAvailable(_ context: ProviderFetchContext) async -> Bool {
// Cheap file existence check; no SQLite open.
let homeURL = context.env["HOME"]
.flatMap { $0.isEmpty ? nil : URL(fileURLWithPath: $0, isDirectory: true) }
?? FileManager.default.homeDirectoryForCurrentUser
return AntigravityOfflineStore.hasOfflineData(home: homeURL, env: context.env)
}

func fetch(_ context: ProviderFetchContext) async throws -> ProviderFetchResult {
let homeURL = context.env["HOME"]
.flatMap { $0.isEmpty ? nil : URL(fileURLWithPath: $0, isDirectory: true) }
?? FileManager.default.homeDirectoryForCurrentUser
let count = AntigravityOfflineStore.countConversations(home: homeURL, env: context.env)
guard count > 0 else {
throw AntigravityStatusProbeError.notRunning
}
let window = RateWindow(
usedPercent: 0,
windowMinutes: nil,
resetsAt: nil,
resetDescription: nil)
let offlineWindow = NamedRateWindow(
id: "antigravity-offline-conversations",
title: "Offline · \(count) conversation" + (count == 1 ? "" : "s"),
window: window,
usageKnown: false)
let snapshot = UsageSnapshot(
primary: nil,
secondary: nil,
tertiary: nil,
extraRateWindows: [offlineWindow],
updatedAt: Date(),
identity: ProviderIdentitySnapshot(
providerID: .antigravity,
accountEmail: AntigravitySelectedAccountGuard.selectedAccountEmail(context: context),
accountOrganization: nil,
loginMethod: "offline"))
Comment on lines +831 to +835

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Do not attribute ambient offline data to the selected account

When a saved Antigravity account is selected and the live/OAuth strategies fail, this fallback counts files in the ambient HOME/GEMINI_CLI_HOME store but assigns the selected credential's email to the resulting snapshot. The nearby account guard explicitly notes that only OAuth is account-scoped, so these conversations can belong to another locally signed-in account while being displayed and persisted under the selected account; either disable this fallback for selected accounts or leave its identity unscoped rather than fabricating the selected email.

Useful? React with 👍 / 👎.

return self.makeResult(usage: snapshot, sourceLabel: "offline")
}

func shouldFallback(on _: Error, context _: ProviderFetchContext) -> Bool {
// Offline is terminal; no further fallback.
false
}
}

/// Guards ambient Antigravity snapshots against the explicitly selected account.
///
/// The local desktop probe and the ``agy`` CLI HTTPS server report whichever
Expand Down
Loading