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
18 changes: 9 additions & 9 deletions Sources/CodexBar/Config/CodexBarConfigMigrator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@ struct CodexBarConfigMigrator {
state: inout MigrationState,
mutate: (inout ProviderConfig) -> Bool)
{
guard let index = config.providers.firstIndex(where: { $0.id == provider }) else { return }
guard let index = config.providers.firstIndex(where: { $0.id == provider.instanceID }) else { return }
var entry = config.providers[index]
let changed = mutate(&entry)
if changed {
Expand Down Expand Up @@ -351,21 +351,21 @@ struct CodexBarConfigMigrator {

private static func applyProviderOrder(_ raw: [String], config: CodexBarConfig) -> CodexBarConfig {
let configsByID = Dictionary(uniqueKeysWithValues: config.providers.map { ($0.id, $0) })
var seen: Set<UsageProvider> = []
var seen: Set<ProviderInstanceID> = []
var ordered: [ProviderConfig] = []
ordered.reserveCapacity(config.providers.count)

for rawValue in raw {
guard let provider = UsageProvider(rawValue: rawValue),
let entry = configsByID[provider],
!seen.contains(provider)
guard let instanceID = ProviderInstanceID(rawValue: rawValue),
let entry = configsByID[instanceID],
!seen.contains(instanceID)
else { continue }
seen.insert(provider)
seen.insert(instanceID)
ordered.append(entry)
}

for provider in UsageProvider.allCases where !seen.contains(provider) {
ordered.append(configsByID[provider] ?? ProviderConfig(id: provider))
for provider in UsageProvider.allCases where !seen.contains(provider.instanceID) {
ordered.append(configsByID[provider.instanceID] ?? ProviderConfig(id: provider.instanceID))
}

var updated = config
Expand All @@ -379,7 +379,7 @@ struct CodexBarConfigMigrator {
{
var updated = config
for index in updated.providers.indices {
let provider = updated.providers[index].id
guard let provider = updated.providers[index].id.firstPartyProvider else { continue }
let meta = ProviderDescriptorRegistry.descriptor(for: provider).metadata
if let value = toggles[meta.cliName] {
updated.providers[index].enabled = value
Expand Down
2 changes: 1 addition & 1 deletion Sources/CodexBar/FleetAccountMenuProjection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ enum FleetAccountMenuPlanner {
hasLocalUsage: Bool) -> FleetAccountMenuProjection
{
let remote = snapshots
.filter { $0.provider == provider && $0.deviceID != currentDeviceID }
.filter { $0.provider == provider.instanceID && $0.deviceID != currentDeviceID }
let freshestByAccount = Dictionary(grouping: remote, by: \.accountKey)
.compactMap { _, candidates in candidates.max(by: self.isOlder) }
.sorted(by: self.isNewer)
Expand Down
4 changes: 2 additions & 2 deletions Sources/CodexBar/MenuBarLayoutEditor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ struct MenuBarLayoutEditor: View {
}

private var providers: [UsageProvider] {
self.store.enabledProvidersForDisplay()
self.store.enabledFirstPartyProvidersForDisplay()
}

private var scopedProvider: UsageProvider? {
Expand Down Expand Up @@ -627,7 +627,7 @@ private struct MenuBarLayoutPreview: View {

var body: some View {
let provider = self.provider ?? .codex
let snapshot = self.store.snapshot(for: provider)
let snapshot = self.store.snapshot(for: provider.instanceID)
let data = snapshot.map { self.liveData(provider: provider, snapshot: $0) }
?? self.representativeData(provider: provider)
let icon = ProviderBrandIcon.image(for: provider)
Expand Down
19 changes: 10 additions & 9 deletions Sources/CodexBar/MenuCardRefreshMonitor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ final class MenuCardRefreshMonitor {
private var globalManualRefreshInFlight = false
/// Providers with an individual manual refresh in flight. Concurrent entries are allowed so
/// refreshing one provider does not stall or unfreeze another.
private var manualRefreshProviders: Set<UsageProvider> = []
private var frozenManualRefreshModels: [UsageProvider: UsageMenuCardView.Model] = [:]
private var manualRefreshProviders: Set<ProviderInstanceID> = []
private var frozenManualRefreshModels: [ProviderInstanceID: UsageMenuCardView.Model] = [:]

/// True while any manual refresh (global or per-provider) is running.
var isManualRefreshInFlight: Bool {
Expand All @@ -41,19 +41,20 @@ final class MenuCardRefreshMonitor {
provider: UsageProvider? = nil)
{
if let provider {
self.frozenManualRefreshModels[provider] = frozenModels[provider]
self.manualRefreshProviders.insert(provider)
self.frozenManualRefreshModels[provider.instanceID] = frozenModels[provider]
self.manualRefreshProviders.insert(provider.instanceID)
} else {
self.frozenManualRefreshModels = frozenModels
self.frozenManualRefreshModels = Dictionary(
uniqueKeysWithValues: frozenModels.map { ($0.key.instanceID, $0.value) })
self.globalManualRefreshInFlight = true
}
}

/// Balances a `beginManualRefresh` with the same `provider` argument (nil ends the global refresh).
func endManualRefresh(for provider: UsageProvider? = nil) {
if let provider {
self.manualRefreshProviders.remove(provider)
self.frozenManualRefreshModels[provider] = nil
self.manualRefreshProviders.remove(provider.instanceID)
self.frozenManualRefreshModels[provider.instanceID] = nil
} else {
self.globalManualRefreshInFlight = false
self.frozenManualRefreshModels.removeAll(keepingCapacity: true)
Expand All @@ -67,7 +68,7 @@ final class MenuCardRefreshMonitor {
}

func isManualRefreshInFlight(for provider: UsageProvider) -> Bool {
self.manualRefreshProviders.contains(provider) ||
self.manualRefreshProviders.contains(provider.instanceID) ||
(self.globalManualRefreshInFlight && self.isProviderRefreshActive(provider))
}

Expand All @@ -76,7 +77,7 @@ final class MenuCardRefreshMonitor {
fallback: UsageMenuCardView.Model) -> UsageMenuCardView.Model
{
guard !self.isManualRefreshInFlight(for: provider) else {
guard let frozen = self.frozenManualRefreshModels[provider] else {
guard let frozen = self.frozenManualRefreshModels[provider.instanceID] else {
return fallback
}
if fallback.hasCompatibleTrackedLayout(with: frozen) {
Expand Down
4 changes: 2 additions & 2 deletions Sources/CodexBar/MenuContent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ struct StatusIconView: View {
}

private var accessibilityValue: String {
let snapshot = self.store.snapshot(for: self.provider)
let snapshot = self.store.snapshot(for: self.provider.instanceID)
guard let snap = snapshot else {
return L("No data")
}
Expand All @@ -238,7 +238,7 @@ struct StatusIconView: View {

private var icon: NSImage {
let now = Date()
let snapshot = self.store.snapshot(for: self.provider)
let snapshot = self.store.snapshot(for: self.provider.instanceID)
let remaining = snapshot.map {
IconRemainingResolver.resolvedRemaining(
snapshot: $0,
Expand Down
22 changes: 11 additions & 11 deletions Sources/CodexBar/MenuDescriptor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ struct MenuDescriptor {
} else {
var addedUsage = false

for enabledProvider in store.enabledProviders() {
for enabledProvider in store.enabledFirstPartyProviders() {
sections.append(Self.usageSection(for: enabledProvider, store: store, settings: settings))
addedUsage = true
}
Expand Down Expand Up @@ -234,7 +234,7 @@ struct MenuDescriptor {
}()
entries.append(.text(headlineText, .headline))

if let snap = store.snapshot(for: provider) {
if let snap = store.snapshot(for: provider.instanceID) {
let resetStyle = settings.resetTimeDisplayStyle
let labels = Self.rateWindowLabels(provider: provider, metadata: meta, snapshot: snap)
let crofShowsCreditsOnly = provider == .crof && snap.secondary == nil
Expand Down Expand Up @@ -360,7 +360,7 @@ struct MenuDescriptor {
store: store,
settings: settings,
metadata: meta,
snapshot: store.snapshot(for: provider))
snapshot: store.snapshot(for: provider.instanceID))
ProviderCatalog.implementation(for: provider)?
.appendUsageMenuEntries(context: usageContext, entries: &entries)

Expand Down Expand Up @@ -460,7 +460,7 @@ struct MenuDescriptor {
settings: SettingsStore,
account: AccountInfo) -> Section?
{
let snapshot = store.snapshot(for: provider)
let snapshot = store.snapshot(for: provider.instanceID)
let metadata = store.metadata(for: provider)
let entries = Self.accountEntries(
provider: provider,
Expand Down Expand Up @@ -574,9 +574,9 @@ struct MenuDescriptor {
}

private static func accountProviderForCombined(store: UsageStore) -> UsageProvider? {
for provider in store.enabledProviders() {
for provider in store.enabledFirstPartyProviders() {
let metadata = store.metadata(for: provider)
if store.snapshot(for: provider)?.identity(for: provider) != nil {
if store.snapshot(for: provider.instanceID)?.identity(for: provider.instanceID) != nil {
return provider
}
if metadata.usesAccountFallback {
Expand All @@ -594,7 +594,7 @@ struct MenuDescriptor {
codexAccountPromotionCoordinator: CodexAccountPromotionCoordinator?) -> Section
{
var entries: [Entry] = []
let targetProvider = provider ?? store.enabledProviders().first
let targetProvider = provider ?? store.enabledFirstPartyProviders().first
let metadata = targetProvider.map { store.metadata(for: $0) }
let fallbackAccount = targetProvider.map { store.accountInfo(for: $0) } ?? account
let hasAccount = self.hasAccount(for: targetProvider, store: store, account: fallbackAccount)
Expand Down Expand Up @@ -667,7 +667,7 @@ struct MenuDescriptor {
}

private static func statusLine(for provider: UsageProvider?, store: UsageStore) -> String? {
let target = provider ?? store.enabledProviders().first
let target = provider ?? store.enabledFirstPartyProviders().first
guard let target,
let status = store.status(for: target),
status.indicator != .none else { return nil }
Expand All @@ -685,15 +685,15 @@ struct MenuDescriptor {
if let provider {
return .switchAccount(provider)
}
if let enabled = store.enabledProviders().first {
if let enabled = store.enabledFirstPartyProviders().first {
return .switchAccount(enabled)
}
return .switchAccount(.codex)
}

private static func hasAccount(for provider: UsageProvider?, store: UsageStore, account: AccountInfo) -> Bool {
let target = provider ?? store.enabledProviders().first ?? .codex
let snapshot = store.snapshot(for: target)
let target = provider ?? store.enabledFirstPartyProviders().first ?? .codex
let snapshot = store.snapshot(for: target.instanceID)
if let email = snapshot?.accountEmail(for: target),
!email.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
{
Expand Down
12 changes: 6 additions & 6 deletions Sources/CodexBar/MenuOpenRefreshPlan.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,19 @@ import CodexBarCore
struct MenuOpenRefreshPlan: Equatable {
struct Inputs {
let refreshAllOnOpen: Bool
let enabledProviders: [UsageProvider]
let visibleProviders: [UsageProvider]
let refreshingProviders: Set<UsageProvider>
let staleProviders: Set<UsageProvider>
let missingProviders: Set<UsageProvider>
let enabledProviders: [ProviderInstanceID]
let visibleProviders: [ProviderInstanceID]
let refreshingProviders: Set<ProviderInstanceID>
let staleProviders: Set<ProviderInstanceID>
let missingProviders: Set<ProviderInstanceID>
}

enum Scheduling: Equatable {
case sequential
case concurrent
}

let providers: [UsageProvider]
let providers: [ProviderInstanceID]
let scheduling: Scheduling
let refreshCodexDashboard: Bool

Expand Down
4 changes: 2 additions & 2 deletions Sources/CodexBar/MenuSwitchFlickerProbe.swift
Original file line number Diff line number Diff line change
Expand Up @@ -268,12 +268,12 @@ enum MenuSwitchFlickerProbe {
/// current selection so the probe performs a real switch, and restores
/// the original selection afterwards.
private func resolveSegments() {
let enabledProviders = self.controller.store.enabledProvidersForDisplay()
let enabledProviders = self.controller.store.enabledFirstPartyProvidersForDisplay()
let includesOverview = self.controller.includesOverviewTab(enabledProviders: enabledProviders)
let selection = self.controller.resolvedSwitcherSelection(
enabledProviders: enabledProviders,
includesOverview: includesOverview)
var segments: [ProviderSwitcherSelection] = enabledProviders.map { .provider($0) }
var segments: [ProviderSwitcherSelection] = enabledProviders.map { .provider($0.instanceID) }
if includesOverview {
segments.insert(.overview, at: 0)
}
Expand Down
43 changes: 24 additions & 19 deletions Sources/CodexBar/PlanUtilizationHistoryStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ struct PlanUtilizationHistoryStore: Sendable {
Self()
}

func load() -> [UsageProvider: PlanUtilizationHistoryBuckets] {
func load() -> [ProviderInstanceID: PlanUtilizationHistoryBuckets] {
self.loadProviderFiles()
}

Expand All @@ -216,11 +216,11 @@ struct PlanUtilizationHistoryStore: Sendable {
/// ~150 ms for mature two-year histories and must not run on the app
/// startup main thread. The returned dictionary is safe to apply on the
/// main actor once decoding completes.
func loadAsync() async -> [UsageProvider: PlanUtilizationHistoryBuckets] {
func loadAsync() async -> [ProviderInstanceID: PlanUtilizationHistoryBuckets] {
await Task.detached(priority: .utility) { self.load() }.value
}

func save(_ providers: [UsageProvider: PlanUtilizationHistoryBuckets]) {
func save(_ providers: [ProviderInstanceID: PlanUtilizationHistoryBuckets]) {
guard let directoryURL = self.directoryURL else { return }
do {
try FileManager.default.createDirectory(
Expand All @@ -230,9 +230,10 @@ struct PlanUtilizationHistoryStore: Sendable {
encoder.dateEncodingStrategy = .iso8601
encoder.outputFormatting = [.sortedKeys]

for provider in UsageProvider.allCases {
let fileURL = self.providerFileURL(for: provider)
let buckets = providers[provider] ?? PlanUtilizationHistoryBuckets()
let knownInstanceIDs = Set(UsageProvider.allCases.map(\.instanceID)).union(providers.keys)
for instanceID in knownInstanceIDs.sorted(by: { $0.rawValue < $1.rawValue }) {
let fileURL = self.providerFileURL(for: instanceID)
let buckets = providers[instanceID] ?? PlanUtilizationHistoryBuckets()
let unscoped = Self.sortedHistories(buckets.unscoped)
let accounts = Self.sortedAccounts(buckets.accounts)
guard !unscoped.isEmpty || !accounts.isEmpty || !buckets.sessionEquivalentWindowPairIdentities.isEmpty
Expand All @@ -255,17 +256,21 @@ struct PlanUtilizationHistoryStore: Sendable {
}
}

private func loadProviderFiles() -> [UsageProvider: PlanUtilizationHistoryBuckets] {
guard self.directoryURL != nil else { return [:] }
private func loadProviderFiles() -> [ProviderInstanceID: PlanUtilizationHistoryBuckets] {
guard let directoryURL = self.directoryURL,
let fileURLs = try? FileManager.default.contentsOfDirectory(
at: directoryURL,
includingPropertiesForKeys: nil)
else { return [:] }

let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601

var output: [UsageProvider: PlanUtilizationHistoryBuckets] = [:]
var output: [ProviderInstanceID: PlanUtilizationHistoryBuckets] = [:]

for provider in UsageProvider.allCases {
let fileURL = self.providerFileURL(for: provider)
guard FileManager.default.fileExists(atPath: fileURL.path) else { continue }
for fileURL in fileURLs where fileURL.pathExtension == "json" {
guard let instanceID = ProviderInstanceID(rawValue: fileURL.deletingPathExtension().lastPathComponent)
else { continue }
guard let data = try? Data(contentsOf: fileURL),
let decoded = try? decoder.decode(ProviderHistoryDocument.self, from: data)
else {
Expand All @@ -277,19 +282,19 @@ struct PlanUtilizationHistoryStore: Sendable {
unscoped: decoded.unscoped,
accounts: decoded.accounts,
sessionEquivalentWindowPairIdentities: decoded.sessionEquivalentWindowPairIdentities)
output[provider] = Self.decodeProvider(history)
output[instanceID] = Self.decodeProvider(history)
}

return output
}

private static func decodeProviders(
_ providers: [String: ProviderHistoryFile]) -> [UsageProvider: PlanUtilizationHistoryBuckets]
_ providers: [String: ProviderHistoryFile]) -> [ProviderInstanceID: PlanUtilizationHistoryBuckets]
{
var output: [UsageProvider: PlanUtilizationHistoryBuckets] = [:]
var output: [ProviderInstanceID: PlanUtilizationHistoryBuckets] = [:]
for (rawProvider, providerHistory) in providers {
guard let provider = UsageProvider(rawValue: rawProvider) else { continue }
output[provider] = Self.decodeProvider(providerHistory)
guard let instanceID = ProviderInstanceID(rawValue: rawProvider) else { continue }
output[instanceID] = Self.decodeProvider(providerHistory)
}
return output
}
Expand Down Expand Up @@ -342,9 +347,9 @@ struct PlanUtilizationHistoryStore: Sendable {
return dir.appendingPathComponent("history", isDirectory: true)
}

private func providerFileURL(for provider: UsageProvider) -> URL {
private func providerFileURL(for instanceID: ProviderInstanceID) -> URL {
let directoryURL = self.directoryURL ?? URL(fileURLWithPath: "/dev/null", isDirectory: true)
return directoryURL.appendingPathComponent("\(provider.rawValue).json", isDirectory: false)
return directoryURL.appendingPathComponent("\(instanceID.rawValue).json", isDirectory: false)
}
}

Expand Down
2 changes: 1 addition & 1 deletion Sources/CodexBar/PreferencesMenuBarPane.swift
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ struct MenuBarPane: View {
}

private var activeProvidersInOrder: [UsageProvider] {
self.store.enabledProviders()
self.store.enabledFirstPartyProviders()
}

private var overviewSelectedProviders: [UsageProvider] {
Expand Down
Loading