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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
25 changes: 19 additions & 6 deletions Sources/CodexBar/StatusItemController+Menu.swift
Original file line number Diff line number Diff line change
Expand Up @@ -565,9 +565,8 @@ extension StatusItemController {
// Rows may be built into a detached scratch menu for in-place reconciliation;
// interaction closures must always reference the live menu they end up serving.
let interactionMenu = captureMenu ?? menu
let overviewProviders = self.settings.reconcileMergedOverviewSelectedProviders(
activeProviders: enabledProviders)
let rows: [(provider: UsageProvider, model: UsageMenuCardView.Model)] = overviewProviders
let providerScopes = self.overviewProviderScopes(enabledProviders: enabledProviders)
let rows: [(provider: UsageProvider, model: UsageMenuCardView.Model)] = providerScopes.visible
.compactMap { provider in
guard let model = self.menuCardModel(for: provider) else { return nil }
guard !model.isOverviewErrorOnly else { return nil }
Expand All @@ -578,12 +577,11 @@ extension StatusItemController {
let t0 = CACurrentMediaTime()
defer { self.logChartRenderDurationIfSlow("addOverviewRows(\(rows.count))", startedAt: t0) }

let spendProviders = overviewProviders.filter { self.settings.costSummaryShowsInline(for: $0) }
let spendModel = self.overviewSpendDashboardModel(providers: spendProviders)
let spendModel = self.overviewSpendDashboardModel(providers: providerScopes.spend)
if !spendModel.groups.isEmpty {
let spendSummary = OverviewSpendSummary(
model: spendModel,
providerCount: spendProviders.count)
providerCount: providerScopes.spend.count)
let summaryItem = self.makeMenuCardItem(
OverviewSpendSummaryCardView(
summary: spendSummary,
Expand Down Expand Up @@ -639,6 +637,21 @@ extension StatusItemController {
return true
}

func overviewProviderScopes(
enabledProviders: [UsageProvider]) -> (visible: [UsageProvider], spend: [UsageProvider])
{
let visible = self.settings.reconcileMergedOverviewSelectedProviders(
activeProviders: enabledProviders)
var seenSpendProviders = Set<UsageProvider>()
let spend = enabledProviders.filter { provider in
seenSpendProviders.insert(provider).inserted &&
self.settings.costSummaryShowsInline(for: provider)
}
return (
visible: visible,
spend: spend)
}

private func addOverviewEmptyState(to menu: NSMenu, enabledProviders: [UsageProvider]) {
let resolvedProviders = self.settings.resolvedMergedOverviewProviders(
activeProviders: enabledProviders,
Expand Down
6 changes: 3 additions & 3 deletions Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -962,7 +962,7 @@ struct ProviderArchitectureGatekeeperTests {
reason: "The memory-pressure debug fixture installs its synthetic entry in the Codex cache slot."),
SuppressedProviderReference(
path: "Sources/CodexBar/StatusItemController+Menu.swift",
line: 1113,
line: 1126,
anchor: "controller.refreshOpenMenuIfStillVisible(menu, provider: .codex)",
expectedProviderIDs: ["codex"],
reason: "This provider-specific app branch passes its already-selected identity to a shared helper."),
Expand Down Expand Up @@ -2482,15 +2482,15 @@ struct ProviderArchitectureGatekeeperTests {
reason: "This exact app-runtime bridge coordinates provider-owned state through the shared controller."),
AllowedProviderConstruct(
path: "Sources/CodexBar/StatusItemController+Menu.swift",
line: 1146,
line: 1159,
anchor: "return .provider((self.resolvedMenuProvider(enabledProviders: enabledProviders) ?? .codex).instanceID)",
expectedProviderIDs: ["codex"],
expectedReferenceCount: 1,
expectedReferenceFingerprint: ["codex@0"],
reason: "This exact app-runtime bridge coordinates provider-owned state through the shared controller."),
AllowedProviderConstruct(
path: "Sources/CodexBar/StatusItemController+Menu.swift",
line: 1159,
line: 1172,
anchor: "return self.store.enabledFirstPartyProvidersForDisplay().first ?? .codex",
expectedProviderIDs: ["codex"],
expectedReferenceCount: 1,
Expand Down
107 changes: 107 additions & 0 deletions Tests/CodexBarTests/StatusMenuOverviewSpendTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,113 @@ extension StatusMenuTests {
#expect(summary.isPartial)
}

@Test
func `overview keeps six visible providers while accounting for all seven connected providers`() throws {
self.disableMenuCardsForTesting()
let settings = self.makeSettings()
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.mergeIcons = true
settings.selectedMenuProvider = .claude
settings.mergedMenuLastSelectedWasOverview = true
settings.costUsageEnabled = true
settings.costSummaryDisplayStyle = .both
let connected: [UsageProvider] = [
.openai,
.claude,
.gemini,
.antigravity,
.openrouter,
.grok,
.codex,
]
for provider in UsageProvider.allCases {
guard let metadata = ProviderRegistry.shared.metadata[provider] else { continue }
settings.setProviderEnabled(provider: provider, metadata: metadata, enabled: connected.contains(provider))
}

let store = self.makeCodexStore(settings: settings, dashboardAuthorized: false)
let enabledRoster = store.enabledFirstPartyProvidersForDisplay()
#expect(Set(enabledRoster) == Set(connected))
let now = Date()
let components = Calendar.current.dateComponents([.year, .month, .day], from: now)
let year = try #require(components.year)
let month = try #require(components.month)
let dayOfMonth = try #require(components.day)
let day = String(format: "%04d-%02d-%02d", year, month, dayOfMonth)
for provider in enabledRoster {
store._setSnapshotForTesting(
UsageSnapshot(
primary: RateWindow(
usedPercent: 25,
windowMinutes: 300,
resetsAt: now.addingTimeInterval(3600),
resetDescription: nil),
secondary: nil,
updatedAt: now),
provider: provider)
}
func snapshot(cost: Double) -> CostUsageTokenSnapshot {
CostUsageTokenSnapshot(
sessionTokens: nil,
sessionCostUSD: nil,
last30DaysTokens: 0,
last30DaysCostUSD: cost,
costProvenance: .vendorMetered,
daily: [
CostUsageDailyReport.Entry(
date: day,
inputTokens: 0,
outputTokens: 0,
totalTokens: 0,
requestCount: 1,
costUSD: cost,
modelsUsed: nil,
modelBreakdowns: nil),
],
updatedAt: now)
}
let controller = StatusItemController(
store: store,
settings: settings,
account: UsageFetcher().loadAccountInfo(),
updater: DisabledUpdaterController(),
preferencesSelection: PreferencesSelection(),
statusBar: self.makeStatusBarForTesting())
defer { controller.releaseStatusItemsForTesting() }

let scopes = controller.overviewProviderScopes(enabledProviders: enabledRoster)
let hiddenProvider = try #require(scopes.spend.first { !scopes.visible.contains($0) })
let pricedProviders = [scopes.visible[0], scopes.visible[1], hiddenProvider]
store._setTokenSnapshotForTesting(snapshot(cost: 35.09), provider: pricedProviders[0])
store._setTokenSnapshotForTesting(snapshot(cost: 39.79), provider: pricedProviders[1])
store._setTokenSnapshotForTesting(snapshot(cost: 10.12), provider: pricedProviders[2])
store._setTokenSnapshotForTesting(snapshot(cost: 1000), provider: .cursor)

let duplicateScopes = controller.overviewProviderScopes(
enabledProviders: enabledRoster + [enabledRoster[0]])
let model = controller.overviewSpendDashboardModel(providers: scopes.spend, now: now)
let summary = OverviewSpendSummary(model: model, providerCount: scopes.spend.count)
let menu = controller.makeMenu()
controller.menuWillOpen(menu)
defer { controller.menuDidClose(menu) }
let ids = menu.items.compactMap { $0.representedObject as? String }
let overviewRows = ids.filter { $0.hasPrefix("overviewRow-") }

#expect(scopes.visible.count == 6)
#expect(!scopes.visible.contains(hiddenProvider))
#expect(scopes.spend == enabledRoster)
#expect(duplicateScopes.spend == enabledRoster)
#expect(Set(overviewRows) == Set(scopes.visible.map { "overviewRow-\($0.rawValue)" }))
#expect(overviewRows.count == 6)
#expect(ids.contains("overviewSpendSummary"))
#expect(Set(model.groups.first?.providers.map(\.provider) ?? []) == Set(pricedProviders))
#expect(abs((model.groups.first?.totalCost ?? -1) - 85) < 1e-9)
#expect(summary.primarySpendText == "~$85.00")
#expect(summary.providerCoverageText == "3 of 7 subscriptions have spend")
#expect(summary.isPartial)
}

@Test
func `overview spend follows the inline display preference`() throws {
for (style, enabled) in [
Expand Down