Skip to content
11 changes: 4 additions & 7 deletions Sources/CodexBar/PreferencesSpendDashboardPane.swift
Original file line number Diff line number Diff line change
Expand Up @@ -142,17 +142,11 @@ func spendDashboardModelHistoryPresentation(
struct SpendDashboardPane: View {
@Bindable var settings: SettingsStore
@Bindable var store: UsageStore
@State private var controller: SpendDashboardController
@State private var isVisible = false

init(settings: SettingsStore, store: UsageStore) {
self.settings = settings
self.store = store
self._controller = State(initialValue: SpendDashboardController(requestBuilder: { mode in
await SpendDashboardSource.makeRequest(settings: settings, store: store, mode: mode)
}, cachedLoader: { request in
await SpendDashboardSource.loadCached(request)
}))
}

var body: some View {
Expand Down Expand Up @@ -194,7 +188,6 @@ struct SpendDashboardPane: View {
}
.onDisappear {
self.isVisible = false
self.controller.stop()
}
.onReceive(NotificationCenter.default.publisher(for: .NSCalendarDayChanged)) { _ in
self.controller.refreshDateWindow()
Expand All @@ -211,6 +204,10 @@ struct SpendDashboardPane: View {
SpendDashboardSource.configuration(settings: self.settings, store: self.store)
}

private var controller: SpendDashboardController {
self.store.sharedSpendDashboardController()
}

private var header: some View {
HStack(alignment: .top, spacing: 16) {
VStack(alignment: .leading, spacing: 4) {
Expand Down
401 changes: 269 additions & 132 deletions Sources/CodexBar/SpendDashboardController.swift

Large diffs are not rendered by default.

8 changes: 6 additions & 2 deletions Sources/CodexBar/SpendDashboardModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -347,9 +347,13 @@ struct SpendDashboardModel: Equatable, Sendable {
hideNativeCodexWhenOpenCodexPresent: Bool) -> [ProviderInput]
{
var filtered = inputs.filter { !hiddenSourceIDs.contains($0.id) }
let hasOpenCodex = filtered.contains { $0.sourceKind == .openCodex }
// Provider-specific by design: only a canonical OpenCodex Codex row may replace native Codex rows.
let hasOpenCodex = filtered.contains {
$0.id == Self.openCodexSourceID &&
$0.provider == .codex &&
$0.sourceKind == .openCodex
}
if hideNativeCodexWhenOpenCodexPresent, hasOpenCodex {
// Provider-specific by design: the OpenCodex source can explicitly replace native Codex rows.
filtered.removeAll { $0.sourceKind == .native && $0.provider == .codex }
}
return filtered
Expand Down
194 changes: 194 additions & 0 deletions Sources/CodexBar/SpendDashboardPublication.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
import CodexBarCore
import Foundation

struct SpendSourcePublication: Sendable, Equatable {
enum Role: Sendable, Equatable {
case subscription
case enrichment
}

enum State: Sendable, Equatable {
case loading
case available
case confirmedEmpty
case unavailable
case staleLastKnown
}

let id: String
let provider: UsageProvider?
let displayName: String
let role: Role
let state: State
}

struct SpendDashboardPublication: Sendable {
let revision: UInt64
let generation: UInt64
let configuration: SpendDashboardConfiguration?
let loadedAt: Date
let isRefreshing: Bool
let inputs: [SpendDashboardModel.ProviderInput]
let sources: [SpendSourcePublication]

static let empty = SpendDashboardPublication(
revision: 0,
generation: 0,
configuration: nil,
loadedAt: .distantPast,
isRefreshing: false,
inputs: [],
sources: [])

func model(
requestedDays: Int,
now: Date,
calendar: Calendar,
preferredCurrencyCode: String,
hiddenSourceIDs: Set<String> = [],
hideNativeCodexWhenOpenCodexPresent: Bool = false,
selectedDay: Date? = nil,
providerScope: Set<UsageProvider>? = nil) -> SpendDashboardModel
{
let staleSourceIDs = Set(self.sources.compactMap { source in
source.state == .staleLastKnown ? source.id : nil
})
let inputs = self.inputs.filter { input in
(providerScope?.contains(input.provider) ?? true) && !staleSourceIDs.contains(input.id)
}
return SpendDashboardModel.build(
inputs: inputs,
requestedDays: requestedDays,
now: now,
calendar: calendar,
preferredCurrencyCode: preferredCurrencyCode,
hiddenSourceIDs: hiddenSourceIDs,
hideNativeCodexWhenOpenCodexPresent: hideNativeCodexWhenOpenCodexPresent,
selectedDay: selectedDay)
}

func subscriptionCount(
providerScope: Set<UsageProvider>,
hiddenSourceIDs: Set<String> = [],
hideNativeCodexWhenOpenCodexPresent: Bool = false) -> Int
{
providerScope.reduce(into: 0) { count, provider in
let rosterSources = self.subscriptionRosterSources(for: provider)
let coverageSources = self.coverageSources(
for: provider,
hiddenSourceIDs: hiddenSourceIDs,
hideNativeCodexWhenOpenCodexPresent: hideNativeCodexWhenOpenCodexPresent)
if rosterSources.isEmpty, coverageSources.isEmpty {
count += hiddenSourceIDs.contains(provider.rawValue) ? 0 : 1
} else {
count += coverageSources.count
}
}
}

func knownCostSubscriptionCount(
model: SpendDashboardModel,
providerScope: Set<UsageProvider>,
hiddenSourceIDs: Set<String> = [],
hideNativeCodexWhenOpenCodexPresent: Bool = false) -> Int
{
let knownInputIDs = Set(model.groups.flatMap(\.providers).compactMap { row in
row.totalCost == nil ? nil : row.id
})
return self.knownSubscriptionCount(
knownInputIDs: knownInputIDs,
providerScope: providerScope,
hiddenSourceIDs: hiddenSourceIDs,
hideNativeCodexWhenOpenCodexPresent: hideNativeCodexWhenOpenCodexPresent)
}

func knownTokenSubscriptionCount(
model: SpendDashboardModel,
providerScope: Set<UsageProvider>,
hiddenSourceIDs: Set<String> = [],
hideNativeCodexWhenOpenCodexPresent: Bool = false) -> Int
{
let knownInputIDs = Set(model.groups.flatMap(\.providers).compactMap { row in
row.totalTokens == nil ? nil : row.id
})
return self.knownSubscriptionCount(
knownInputIDs: knownInputIDs,
providerScope: providerScope,
hiddenSourceIDs: hiddenSourceIDs,
hideNativeCodexWhenOpenCodexPresent: hideNativeCodexWhenOpenCodexPresent)
}

private func knownSubscriptionCount(
knownInputIDs: Set<String>,
providerScope: Set<UsageProvider>,
hiddenSourceIDs: Set<String>,
hideNativeCodexWhenOpenCodexPresent: Bool) -> Int
{
providerScope.reduce(into: 0) { count, provider in
count += self.coverageSources(
for: provider,
hiddenSourceIDs: hiddenSourceIDs,
hideNativeCodexWhenOpenCodexPresent: hideNativeCodexWhenOpenCodexPresent)
.count { source in
source.state == .confirmedEmpty ||
(source.state == .available && knownInputIDs.contains(source.id))
}
}
}

private func subscriptionRosterSources(for provider: UsageProvider) -> [SpendSourcePublication] {
self.sources.filter { $0.provider == provider && $0.role == .subscription }
}

private func coverageSources(
for provider: UsageProvider,
hiddenSourceIDs: Set<String>,
hideNativeCodexWhenOpenCodexPresent: Bool) -> [SpendSourcePublication]
{
let rosterSources = self.subscriptionRosterSources(for: provider)
.filter { !hiddenSourceIDs.contains($0.id) }
// Provider-specific by design: OpenCodex replaces Codex coverage only with a canonical Codex payload.
guard provider == .codex else { return rosterSources }
let visibleOpenCodexInputIDs: Set<String> = Set(self.inputs.compactMap { input -> String? in
guard input.provider == .codex,
input.sourceKind == .openCodex,
!hiddenSourceIDs.contains(input.id)
else { return nil }
return input.id
})
let inputBackedEnrichmentSources = self.sources.filter {
$0.provider == .codex &&
$0.role == .enrichment &&
visibleOpenCodexInputIDs.contains($0.id)
}
let canonicalReplacement = inputBackedEnrichmentSources.first {
$0.id == SpendDashboardModel.openCodexSourceID
}
if hideNativeCodexWhenOpenCodexPresent,
let canonicalReplacement,
canonicalReplacement.state == SpendSourcePublication.State.available
{
return [canonicalReplacement]
}
if rosterSources.isEmpty, !inputBackedEnrichmentSources.isEmpty {
return inputBackedEnrichmentSources
}
// Provider-specific by design: only canonical Codex enrichment can replace Codex subscription coverage.
guard self.subscriptionRosterSources(for: provider).isEmpty,
!hiddenSourceIDs.contains(SpendDashboardModel.openCodexSourceID),
let openCodexObservation = self.sources.first(where: {
$0.id == SpendDashboardModel.openCodexSourceID &&
$0.provider == .codex &&
$0.role == .enrichment
})
else { return rosterSources }
let hasCodexReplacementInput = self.inputs.contains {
$0.id == SpendDashboardModel.openCodexSourceID &&
$0.provider == .codex &&
$0.sourceKind == .openCodex
}
return hasCodexReplacementInput || openCodexObservation.state == .confirmedEmpty
? [openCodexObservation]
: rosterSources
}
}
122 changes: 122 additions & 0 deletions Sources/CodexBar/SpendDashboardSource+OpenCodex.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import CodexBarCore
import Foundation

extension SpendDashboardSource {
static func mergingOpenCodexInputs(
_ inputs: [SpendDashboardModel.ProviderInput],
request: SpendDashboardLoadRequest) -> [SpendDashboardModel.ProviderInput]
{
self.mergingOpenCodexInputsWithObservation(inputs, request: request).inputs
}

static func mergingOpenCodexInputsWithObservation(
_ inputs: [SpendDashboardModel.ProviderInput],
request: SpendDashboardLoadRequest,
environment: [String: String] = ProcessInfo.processInfo.environment,
entryLoader: ((URL) throws -> [OpenCodexUsageEntry])? = nil) -> (
inputs: [SpendDashboardModel.ProviderInput],
observation: SpendDashboardLoadResult.OpenCodexObservation)
{
guard request.configuration.openCodexUsageLogsEnabled,
!request.configuration.hiddenSourceIDs.contains(SpendDashboardModel.openCodexSourceID)
else {
return (
inputs.filter { $0.id != SpendDashboardModel.openCodexSourceID },
.disabled)
}
guard let logURL = OpenCodexUsageLog.usageLogURL(environment: environment) else {
return (inputs.filter { $0.id != SpendDashboardModel.openCodexSourceID }, .unavailable)
}
let store = OpenCodexUsageStore(cacheRoot: OpenCodexUsageLog.cacheRoot())
let entries: [OpenCodexUsageEntry]
do {
entries = try entryLoader?(logURL) ?? store.loadEntries(logURL: logURL)
} catch {
return (inputs.filter { $0.id != SpendDashboardModel.openCodexSourceID }, .unavailable)
}
guard !entries.isEmpty else {
return (inputs.filter { $0.id != SpendDashboardModel.openCodexSourceID }, .confirmedEmpty)
}

let snapshots = OpenCodexUsageFanOut.snapshotsBySubscription(
entries: entries,
now: request.now,
historyDays: Self.scanDays,
calendar: request.configuration.bucketCalendar)
var merged = inputs.filter { $0.id != SpendDashboardModel.openCodexSourceID }
var published = false

for (provider, supplement) in snapshots {
guard Self.shouldPublishOpenCodexSnapshot(supplement) else { continue }
published = true
// Provider-specific by design: hide-native keeps OpenCodex on its own Codex row
// so visibleInputs can drop overlapping native Codex snapshots.
if provider == .codex,
request.configuration.hideNativeCodexCostWhenOpenCodexPresent
{
merged.append(SpendDashboardModel.ProviderInput(
id: SpendDashboardModel.openCodexSourceID,
provider: provider,
displayName: ProviderDescriptorRegistry.descriptor(for: provider).metadata.displayName,
snapshot: supplement,
sourceKind: .openCodex))
continue
}
if let index = Self.preferredMergeIndex(for: provider, in: merged) {
merged[index] = Self.mergeProviderInput(
merged[index],
supplement: supplement,
request: request)
} else {
merged.append(SpendDashboardModel.ProviderInput(
provider: provider,
displayName: ProviderDescriptorRegistry.descriptor(for: provider).metadata.displayName,
snapshot: supplement,
sourceKind: .openCodex))
}
}
return (merged, published ? .available : .confirmedEmpty)
}

static func preferredMergeIndex(
for provider: UsageProvider,
in inputs: [SpendDashboardModel.ProviderInput]) -> Int?
{
// Provider-specific by design: OpenCodex fan-out merges into the native Codex subscription row when exactly one
// exists.
if provider == .codex {
let codexIndices = inputs.indices.filter { inputs[$0].provider == .codex }
guard codexIndices.count == 1 else { return nil }
return codexIndices.first
}
let matching = inputs.indices.filter { inputs[$0].provider == provider }
guard matching.count == 1 else {
return inputs.firstIndex(where: { $0.provider == provider && $0.sourceKind == .native })
}
return matching.first
}

private static func mergeProviderInput(
_ input: SpendDashboardModel.ProviderInput,
supplement: CostUsageTokenSnapshot,
request: SpendDashboardLoadRequest) -> SpendDashboardModel.ProviderInput
{
SpendDashboardModel.ProviderInput(
id: input.id,
provider: input.provider,
displayName: input.displayName,
modelProviderName: input.modelProviderName,
snapshot: OpenCodexUsageFanOut.mergeSnapshots(
input.snapshot,
supplement,
now: request.now,
historyDays: self.scanDays,
calendar: request.configuration.bucketCalendar),
tokenActivityCache: input.tokenActivityCache,
sourceKind: input.sourceKind)
}

static func shouldPublishOpenCodexSnapshot(_ snapshot: CostUsageTokenSnapshot) -> Bool {
!snapshot.daily.isEmpty || !snapshot.sessions.isEmpty
}
}
13 changes: 10 additions & 3 deletions Sources/CodexBar/StatusItemController+Menu.swift
Original file line number Diff line number Diff line change
Expand Up @@ -577,11 +577,18 @@ extension StatusItemController {
let t0 = CACurrentMediaTime()
defer { self.logChartRenderDurationIfSlow("addOverviewRows(\(rows.count))", startedAt: t0) }

let spendModel = self.overviewSpendDashboardModel(providers: providerScopes.spend)
if !spendModel.groups.isEmpty {
let spendProviders = providerScopes.spend
let spendModel = self.overviewSpendDashboardModel(providers: spendProviders)
let spendProviderCount = self.overviewSpendSubscriptionCount(providers: spendProviders)
if spendProviderCount > 0 {
let knownCounts = self.overviewSpendKnownSubscriptionCounts(
providers: spendProviders,
model: spendModel)
let spendSummary = OverviewSpendSummary(
model: spendModel,
providerCount: providerScopes.spend.count)
providerCount: spendProviderCount,
knownCostProviderCount: knownCounts.cost,
knownTokenProviderCount: knownCounts.tokens)
let summaryItem = self.makeMenuCardItem(
OverviewSpendSummaryCardView(
summary: spendSummary,
Expand Down
Loading