-
Notifications
You must be signed in to change notification settings - Fork 1.8k
fix(spend): silent refresh and invalidation coverage #3106
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
479e50f
b2eba0f
6e8884b
8a2cec5
cc8f2fe
eac5823
e7332d7
24dbece
1a34d46
f91952d
387687b
99cadf9
d3b0490
136687b
13fd5cc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,147 @@ | ||
| { | ||
| "generatedAt" : "2026-08-23T05:39:37Z", | ||
| "host" : { | ||
| "codexBarVersion" : null, | ||
| "refreshIntervalSeconds" : 0 | ||
| }, | ||
| "providers" : [ | ||
| { | ||
| "cost" : null, | ||
| "credits" : null, | ||
| "display" : { | ||
| "accentColor" : "#49A3B0", | ||
| "priority" : "normal", | ||
| "sortKey" : 0 | ||
| }, | ||
| "enabled" : true, | ||
| "error" : { | ||
| "code" : 1, | ||
| "kind" : "provider", | ||
| "message" : "Network error: The operation couldn’t be completed. Operation not permitted" | ||
| }, | ||
| "id" : "codex", | ||
| "identity" : null, | ||
| "name" : "Codex", | ||
| "source" : "auto", | ||
| "status" : null, | ||
| "updatedAt" : "2026-08-23T05:39:36Z", | ||
| "windows" : [ | ||
|
|
||
| ] | ||
| }, | ||
| { | ||
| "cost" : null, | ||
| "credits" : null, | ||
| "display" : { | ||
| "accentColor" : "#3B82F6", | ||
| "priority" : "normal", | ||
| "sortKey" : 10 | ||
| }, | ||
| "enabled" : true, | ||
| "error" : null, | ||
| "id" : "opencodego", | ||
| "identity" : null, | ||
| "name" : "OpenCode Go", | ||
| "source" : "local", | ||
| "status" : null, | ||
| "updatedAt" : "2026-08-23T05:39:32Z", | ||
| "windows" : [ | ||
| { | ||
| "kind" : "session", | ||
| "label" : "5-hour", | ||
| "remainingPercent" : 100, | ||
| "resetAt" : "2026-08-23T10:39:32Z", | ||
| "usedPercent" : 0 | ||
| }, | ||
| { | ||
| "kind" : "weekly", | ||
| "label" : "Weekly", | ||
| "remainingPercent" : 92.1, | ||
| "resetAt" : "2026-08-23T23:59:59Z", | ||
| "usedPercent" : 7.9 | ||
| }, | ||
| { | ||
| "kind" : "tertiary", | ||
| "label" : "Monthly", | ||
| "remainingPercent" : 96, | ||
| "resetAt" : "2026-09-20T04:11:23Z", | ||
| "usedPercent" : 4 | ||
| } | ||
| ] | ||
| }, | ||
| { | ||
| "cost" : null, | ||
| "credits" : null, | ||
| "display" : { | ||
| "accentColor" : "#60BA7E", | ||
| "priority" : "normal", | ||
| "sortKey" : 20 | ||
| }, | ||
| "enabled" : true, | ||
| "error" : { | ||
| "code" : 1, | ||
| "kind" : "provider", | ||
| "message" : "antigravity usage timed out" | ||
| }, | ||
| "id" : "antigravity", | ||
| "identity" : null, | ||
| "name" : "Antigravity", | ||
| "source" : "auto", | ||
| "status" : null, | ||
| "updatedAt" : "2026-08-23T05:39:37Z", | ||
| "windows" : [ | ||
|
|
||
| ] | ||
| }, | ||
| { | ||
| "cost" : null, | ||
| "credits" : null, | ||
| "display" : { | ||
| "accentColor" : "#00BFA5", | ||
| "priority" : "normal", | ||
| "sortKey" : 30 | ||
| }, | ||
| "enabled" : true, | ||
| "error" : { | ||
| "code" : 1, | ||
| "kind" : "provider", | ||
| "message" : "The operation couldn’t be completed. Operation not permitted" | ||
| }, | ||
| "id" : "cursor", | ||
| "identity" : null, | ||
| "name" : "Cursor", | ||
| "source" : "auto", | ||
| "status" : null, | ||
| "updatedAt" : "2026-08-23T05:39:37Z", | ||
| "windows" : [ | ||
|
|
||
| ] | ||
| }, | ||
| { | ||
| "cost" : null, | ||
| "credits" : null, | ||
| "display" : { | ||
| "accentColor" : "#527DF0", | ||
| "priority" : "normal", | ||
| "sortKey" : 40 | ||
| }, | ||
| "enabled" : true, | ||
| "error" : { | ||
| "code" : 1, | ||
| "kind" : "provider", | ||
| "message" : "The operation couldn’t be completed. Operation not permitted" | ||
| }, | ||
| "id" : "deepseek", | ||
| "identity" : null, | ||
| "name" : "DeepSeek", | ||
| "source" : "auto", | ||
| "status" : null, | ||
| "updatedAt" : "2026-08-23T05:39:37Z", | ||
| "windows" : [ | ||
|
|
||
| ] | ||
| } | ||
| ], | ||
| "schemaVersion" : 1, | ||
| "staleAfterSeconds" : 180 | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -354,24 +354,29 @@ enum SpendActivityWeekday { | |
| } | ||
|
|
||
| enum SpendActivityDateFormatting { | ||
| static func mediumDateString(_ date: Date, locale: Locale? = nil) -> String { | ||
| static func mediumDateString(_ date: Date, calendar: Calendar? = nil, locale: Locale? = nil) -> String { | ||
| let formatter = DateFormatter() | ||
| formatter.locale = locale ?? codexBarLocalizedResourceLocale() | ||
| formatter.calendar = calendar ?? Calendar.current | ||
| if let calendar, let timeZone = calendar.timeZone as TimeZone? { | ||
| formatter.timeZone = timeZone | ||
| } | ||
| formatter.dateStyle = .medium | ||
| formatter.timeStyle = .none | ||
| return formatter.string(from: date) | ||
| } | ||
| } | ||
|
|
||
| enum SpendActivityAccessibility { | ||
| static func description(date: Date, value: String, locale: Locale? = nil) -> String { | ||
| "\(SpendActivityDateFormatting.mediumDateString(date, locale: locale)): \(value)" | ||
| static func description(date: Date, value: String, calendar: Calendar? = nil, locale: Locale? = nil) -> String { | ||
| "\(SpendActivityDateFormatting.mediumDateString(date, calendar: calendar, locale: locale)): \(value)" | ||
| } | ||
| } | ||
|
|
||
| struct SpendActivityHeatmapView: View { | ||
| let points: [SpendDashboardModel.TokenActivityPoint] | ||
| let now: Date | ||
| let calendar: Calendar | ||
| let selectedDay: Date? | ||
| let onSelectDay: ((Date?) -> Void)? | ||
|
|
||
|
|
@@ -381,14 +386,16 @@ struct SpendActivityHeatmapView: View { | |
| init( | ||
| points: [SpendDashboardModel.TokenActivityPoint], | ||
| now: Date = Date(), | ||
| calendar: Calendar = .current, | ||
| selectedDay: Date? = nil, | ||
| onSelectDay: ((Date?) -> Void)? = nil) | ||
| { | ||
| self.points = points | ||
| self.now = now | ||
| self.calendar = calendar | ||
| self.selectedDay = selectedDay | ||
| self.onSelectDay = onSelectDay | ||
| self._series = State(initialValue: SpendActivitySeries.make(from: points, now: now)) | ||
| self._series = State(initialValue: SpendActivitySeries.make(from: points, now: now, calendar: calendar)) | ||
| } | ||
|
|
||
| var body: some View { | ||
|
|
@@ -458,7 +465,10 @@ struct SpendActivityHeatmapView: View { | |
| } | ||
| .frame(maxWidth: .infinity, alignment: .leading) | ||
| .onChange(of: self.points) { _, points in | ||
| self.series = SpendActivitySeries.make(from: points, now: self.now) | ||
| self.series = SpendActivitySeries.make(from: points, now: self.now, calendar: self.calendar) | ||
| } | ||
|
Comment on lines
+468
to
+469
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If a user has selected a heatmap day and then changes the bucket time zone, rebuilding Useful? React with 👍 / 👎. |
||
| .onChange(of: self.calendar) { _, calendar in | ||
| self.series = SpendActivitySeries.make(from: self.points, now: self.now, calendar: calendar) | ||
|
Comment on lines
+470
to
+471
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When the dashboard first opens with a bucket time zone different from the system zone, this non-initial Useful? React with 👍 / 👎. |
||
| } | ||
| } | ||
|
|
||
|
|
@@ -666,7 +676,7 @@ private struct SpendActivityDailyGrid: View { | |
| title: self.series.isCovered[index] | ||
| ? UsageFormatter.tokenCountString(self.series.daily[index]) | ||
| : L("Unavailable"), | ||
| subtitle: SpendActivityDateFormatting.mediumDateString(date), | ||
| subtitle: SpendActivityDateFormatting.mediumDateString(date, calendar: self.series.calendar), | ||
| width: width) | ||
| .position( | ||
| x: SpendActivityGridGeometry.tooltipCenterX( | ||
|
|
@@ -750,6 +760,8 @@ private struct SpendActivityDailyGrid: View { | |
| private func monthMarkers(pitch: CGFloat) -> [MonthMarker] { | ||
| let formatter = DateFormatter() | ||
| formatter.locale = codexBarLocalizedResourceLocale() | ||
| formatter.calendar = self.series.calendar | ||
| formatter.timeZone = self.series.calendar.timeZone | ||
| formatter.dateFormat = "MMM" | ||
| var markers: [MonthMarker] = [] | ||
| var lastLabel = "" | ||
|
|
@@ -785,7 +797,8 @@ private struct SpendActivityDailyGrid: View { | |
| } | ||
|
|
||
| private func accessibilityDescription(at index: Int, date: Date) -> String { | ||
| SpendActivityAccessibility.description(date: date, value: self.accessibilityTokenValue(at: index)) | ||
| SpendActivityAccessibility.description( | ||
| date: date, value: self.accessibilityTokenValue(at: index), calendar: self.series.calendar) | ||
| } | ||
|
|
||
| private func accessibilityTokenValue(at index: Int) -> String { | ||
|
|
@@ -886,7 +899,7 @@ private struct SpendActivityWeekGrid: View { | |
| title: self.activity.isCovered[col] | ||
| ? UsageFormatter.tokenCountString(self.activity.values[col]) | ||
| : L("Unavailable"), | ||
| subtitle: SpendActivityDateFormatting.mediumDateString(weekStart), | ||
| subtitle: SpendActivityDateFormatting.mediumDateString(weekStart, calendar: self.series.calendar), | ||
| width: width) | ||
| .position( | ||
| x: SpendActivityGridGeometry.tooltipCenterX( | ||
|
|
@@ -940,7 +953,8 @@ private struct SpendActivityWeekGrid: View { | |
| let value = self.activity.isCovered[index] | ||
| ? UsageFormatter.tokenCountString(self.activity.values[index]) | ||
| : L("Unavailable") | ||
| return SpendActivityAccessibility.description(date: weekStart, value: value) | ||
| return SpendActivityAccessibility.description( | ||
| date: weekStart, value: value, calendar: self.series.calendar) | ||
| } | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -751,6 +751,51 @@ enum SpendDashboardSource { | |
| encoder.append(breakdown.priorityTokens) | ||
| } | ||
| } | ||
| encoder.append(snapshot.hourly.count) | ||
| for entry in snapshot.hourly { | ||
| encoder.append(entry.hour.timeIntervalSinceReferenceDate) | ||
| encoder.append(entry.totalTokens) | ||
| encoder.append(entry.costUSD) | ||
| } | ||
| encoder.append(snapshot.projects.count) | ||
| encoder.append(snapshot.sessions.count) | ||
| for project in snapshot.projects { | ||
| encoder.append(project.name) | ||
| encoder.append(project.path ?? "") | ||
| encoder.append(project.totalTokens) | ||
| encoder.append(project.totalCostUSD) | ||
| encoder.append(project.daily.count) | ||
| for entry in project.daily { | ||
| encoder.append(entry.date) | ||
| encoder.append(entry.costUSD) | ||
| encoder.append(entry.totalTokens) | ||
| encoder.append(entry.inputTokens) | ||
| encoder.append(entry.outputTokens) | ||
| } | ||
| if let breakdowns = project.modelBreakdowns { | ||
| encoder.append(breakdowns.count) | ||
| for breakdown in breakdowns { | ||
| encoder.append(breakdown.modelName) | ||
| encoder.append(breakdown.costUSD) | ||
| encoder.append(breakdown.totalTokens) | ||
| } | ||
| } else { | ||
| encoder.append(0) | ||
| } | ||
| } | ||
| for session in snapshot.sessions { | ||
| encoder.append(session.sessionID) | ||
| encoder.append(session.lastActivity.timeIntervalSinceReferenceDate) | ||
| encoder.append(session.totalTokens) | ||
| encoder.append(session.costUSD) | ||
| encoder.append(session.requestCount) | ||
| encoder.append(session.modelBreakdowns.count) | ||
| for breakdown in session.modelBreakdowns { | ||
| encoder.append(breakdown.modelName) | ||
| encoder.append(breakdown.costUSD) | ||
| encoder.append(breakdown.totalTokens) | ||
| } | ||
| } | ||
| return encoder.finalize() | ||
| } | ||
|
|
||
|
|
@@ -1125,10 +1170,28 @@ final class SpendDashboardController { | |
| Self.isDisplayOnlyConfigurationChange(from: previousConfiguration, to: configuration) | ||
| { | ||
| self.configuration = configuration | ||
| // Provider-specific by design: bucket calendar change renormalizes selected day atomically with new config. | ||
| if let selectedDay = self.selectedDay { | ||
| let newCalendar = CostUsageBucketTimeZone.calendar(identifier: configuration.bucketTimeZoneIdentifier) | ||
| let normalized = newCalendar.startOfDay(for: selectedDay) | ||
| if normalized != selectedDay { | ||
| self.selectedDay = normalized | ||
| } | ||
| } | ||
| self.rebuildModel() | ||
| return | ||
| } | ||
| self.configuration = configuration | ||
| // Normalize selected day when bucket timezone changes, atomically with new configuration. | ||
| if let selectedDay = self.selectedDay, | ||
| previousConfiguration?.bucketTimeZoneIdentifier != configuration.bucketTimeZoneIdentifier | ||
| { | ||
| let newCalendar = CostUsageBucketTimeZone.calendar(identifier: configuration.bucketTimeZoneIdentifier) | ||
|
Comment on lines
+1186
to
+1189
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When Useful? React with 👍 / 👎. |
||
| let normalized = newCalendar.startOfDay(for: selectedDay) | ||
| if normalized != selectedDay { | ||
| self.selectedDay = normalized | ||
| } | ||
| } | ||
| if self.isRefreshing || self.phase.manualRefreshOutstanding, | ||
| let previousConfiguration, | ||
| Self.sameSourceOwnership(previousConfiguration, configuration) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When the configured bucket zone differs from the Mac's system zone, retaining
.currentas the default leavesSpendDashboardCurrencySection'smediumDateString(selectedDay)call inPreferencesSpendDashboardPane.swiftformatting the selected bucket midnight in the system zone. Fresh evidence after the heatmap-specific formatter fixes is this remaining dashboard caption call: selecting August 20 in Kiritimati can still display August 19 on a Honolulu Mac. Pass a calendar using the group's bucket time zone at this call site.Useful? React with 👍 / 👎.