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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
- Settings: split provider pane "Settings" sections into "Menu bar" and "Connection" so metric pickers and auth/cookie/source controls are grouped by topic.

### Fixed
- Codex cost history: keep opening and refreshing the submenu fast as project history grows by comparing only the content it renders. Thanks @Yuxin-Qiao!
- Codex cost history: keep model-less token events explicitly unattributed instead of pricing them as GPT-5 while preserving current turn model attribution. Thanks @hhh2210!
- Gemini: recover expired Workspace and education OAuth sessions when current CLI packages omit `oauth2.js`, with explicit credential and install-path discovery fallbacks. Thanks @Yuxin-Qiao!
- Codex accounts: confirm apparent weekly resets before publishing them and isolate reset detection by stable account ownership, preventing transient full gauges and confetti across same-email workspaces (#2054). Thanks @Yuxin-Qiao!
Expand Down
85 changes: 58 additions & 27 deletions Sources/CodexBar/CostHistoryChartMenuView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -363,8 +363,7 @@ struct CostHistoryChartMenuView: View {
var maxDetailRows = 0
var hasModeDetails = false
for entry in sorted {
guard let costUSD = entry.costUSD, costUSD >= 0 else { continue }
guard let date = self.dateFromDayKey(entry.date) else { continue }
guard let (costUSD, date) = self.chartPointInput(for: entry) else { continue }
let point = Point(
date: date,
costUSD: costUSD,
Expand Down Expand Up @@ -455,6 +454,12 @@ struct CostHistoryChartMenuView: View {
return comps.date
}

private static func chartPointInput(for entry: DailyEntry) -> (costUSD: Double, date: Date)? {
guard let costUSD = entry.costUSD, costUSD >= 0 else { return nil }
guard let date = self.dateFromDayKey(entry.date) else { return nil }
return (costUSD, date)
}

private static func peakPoint(model: Model) -> Point? {
guard let key = model.peakKey else { return nil }
return model.pointsByDateKey[key]
Expand Down Expand Up @@ -769,10 +774,29 @@ extension CostHistoryChartMenuView {
let historyDays: Int
let windowLabel: String?
let totalCostBitPattern: UInt64?
let daily: [DailyEntry]
let hasDailyEntries: Bool
let daily: [VisibleDailyFingerprint]
let projects: [VisibleProjectFingerprint]
}

struct VisibleDailyFingerprint: Equatable {
let date: String
let totalTokens: Int?
let requestCount: Int?
let costBitPattern: UInt64?
let modelBreakdowns: [VisibleModelBreakdownFingerprint]
}

struct VisibleModelBreakdownFingerprint: Equatable {
let modelName: String
let costBitPattern: UInt64?
let totalTokens: Int?
let standardCostBitPattern: UInt64?
let priorityCostBitPattern: UInt64?
let standardTokens: Int?
let priorityTokens: Int?
}

struct VisibleProjectFingerprint: Equatable {
let name: String
let path: String?
Expand All @@ -789,33 +813,22 @@ extension CostHistoryChartMenuView {
let totalCostBitPattern: UInt64?
}

static func renderFingerprint(from snapshot: CostUsageTokenSnapshot) -> RenderFingerprint {
self.makeRenderFingerprint(RenderFingerprintInputs(
static func renderFingerprint(
from snapshot: CostUsageTokenSnapshot,
provider: UsageProvider) -> RenderFingerprint
{
let projects = provider == .codex ? snapshot.projects : []
return RenderFingerprint(
currencyCode: snapshot.currencyCode,
historyDays: snapshot.historyDays,
windowLabel: snapshot.historyLabel,
totalCostUSD: snapshot.last30DaysCostUSD,
daily: snapshot.daily,
projects: snapshot.projects))
}

private struct RenderFingerprintInputs {
let currencyCode: String
let historyDays: Int
let windowLabel: String?
let totalCostUSD: Double?
let daily: [DailyEntry]
let projects: [CostUsageProjectBreakdown]
}

private static func makeRenderFingerprint(_ inputs: RenderFingerprintInputs) -> RenderFingerprint {
RenderFingerprint(
currencyCode: inputs.currencyCode,
historyDays: inputs.historyDays,
windowLabel: inputs.windowLabel,
totalCostBitPattern: inputs.totalCostUSD.map(\.bitPattern),
daily: inputs.daily,
projects: Array(inputs.projects.prefix(self.maxVisibleProjectRows)).map { project in
totalCostBitPattern: snapshot.last30DaysCostUSD.map(\.bitPattern),
hasDailyEntries: !snapshot.daily.isEmpty,
daily: snapshot.daily
.filter { self.chartPointInput(for: $0) != nil }
.sorted { $0.date < $1.date }
.map(self.visibleDailyFingerprint),
projects: Array(projects.prefix(self.maxVisibleProjectRows)).map { project in
let visibleSources = self.visibleProjectSources(project)
return VisibleProjectFingerprint(
name: project.name,
Expand All @@ -833,6 +846,24 @@ extension CostHistoryChartMenuView {
})
}

private static func visibleDailyFingerprint(_ entry: DailyEntry) -> VisibleDailyFingerprint {
VisibleDailyFingerprint(
date: entry.date,
totalTokens: entry.totalTokens,
requestCount: entry.requestCount,
costBitPattern: entry.costUSD.map(\.bitPattern),
modelBreakdowns: self.orderedBreakdownItems(entry.modelBreakdowns ?? []).map { item in
VisibleModelBreakdownFingerprint(
modelName: item.modelName,
costBitPattern: item.costUSD.map(\.bitPattern),
totalTokens: item.totalTokens,
standardCostBitPattern: item.standardCostUSD.map(\.bitPattern),
priorityCostBitPattern: item.priorityCostUSD.map(\.bitPattern),
standardTokens: item.standardCostUSD == nil ? nil : item.standardTokens,
priorityTokens: item.priorityCostUSD == nil ? nil : item.priorityTokens)
})
}

static func _defaultSelectedDateKeyForTesting(provider: UsageProvider, daily: [DailyEntry]) -> String? {
self.defaultSelectedDateKey(model: self.makeModel(provider: provider, daily: daily))
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/CodexBar/StatusItemController+HostedSubmenus.swift
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,7 @@ extension StatusItemController {
guard let snapshot = self.tokenSnapshotForCostHistorySubmenu(provider: provider) else {
return .text("none")
}
return .costHistory(CostHistoryChartMenuView.renderFingerprint(from: snapshot))
return .costHistory(CostHistoryChartMenuView.renderFingerprint(from: snapshot, provider: provider))
}

private func usageHistoryRenderSignature(for provider: UsageProvider) -> String {
Expand Down
Loading