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
147 changes: 147 additions & 0 deletions .github/pr-proof/3106-dashboard-snapshot.json
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
}
1 change: 1 addition & 0 deletions Sources/CodexBar/PreferencesSpendDashboardPane.swift
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,7 @@ struct SpendDashboardPane: View {
SpendDashboardPanel {
SpendActivityHeatmapView(
points: self.controller.model.tokenActivity,
calendar: self.settings.costUsageBucketCalendar,
selectedDay: self.controller.selectedDay,
onSelectDay: { day in
self.controller.selectDay(day)
Expand Down
32 changes: 23 additions & 9 deletions Sources/CodexBar/SpendActivityHeatmap.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment on lines +357 to +360

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Pass the bucket calendar to the selected-day caption

When the configured bucket zone differs from the Mac's system zone, retaining .current as the default leaves SpendDashboardCurrencySection's mediumDateString(selectedDay) call in PreferencesSpendDashboardPane.swift formatting 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 👍 / 👎.

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)?

Expand All @@ -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 {
Expand Down Expand Up @@ -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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Renormalize the selected day after a calendar change

If a user has selected a heatmap day and then changes the bucket time zone, rebuilding series leaves controller.selectedDay at midnight in the old calendar. SpendActivityDaySelection.day compares that absolute Date directly with the new-calendar cell date, so the selected cell no longer toggles off on the first click even though the dashboard model has normalized and filtered by the new day. Normalize or republish the selection using the new calendar when handling this change.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Initialize the heatmap with the bucket calendar

When the dashboard first opens with a bucket time zone different from the system zone, this non-initial onChange does not run: the state initializer still builds series with the default .current calendar, and PreferencesSpendDashboardPane.swift does not pass the configured bucket calendar into the view. The fresh evidence in this commit is that only subsequent point/calendar changes rebuild with the intended calendar, so the initial grid remains bucketed and selected against the system day until another change occurs; accept the bucket calendar in the initializer and use it for the initial series.

Useful? React with 👍 / 👎.

}
}

Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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 = ""
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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)
}
}

Expand Down
63 changes: 63 additions & 0 deletions Sources/CodexBar/SpendDashboardController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}

Expand Down Expand Up @@ -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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Renormalize selections when the system time zone changes

When bucketTimeZoneIdentifier is empty (the system-zone configuration) and macOS changes time zones, this comparison remains false because both identifiers are still empty. Fresh evidence beyond the earlier configured-zone report is the .NSSystemTimeZoneDidChange path in PreferencesSpendDashboardPane, which calls only refreshDateWindow(): that rebuilds with the new .current calendar while selectedDay remains midnight in the old zone, so the model's exact day equality can produce an empty drill-down and the corresponding heatmap cell will not toggle off. Track the effective zone or renormalize the selection during the system-time-zone refresh.

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)
Expand Down
6 changes: 6 additions & 0 deletions Sources/CodexBar/UsageStore+SpendDashboardPublication.swift
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,12 @@ extension UsageStore {
self.scheduleDebouncedTokenPublicationSync()
}

#if DEBUG
var _test_hasPendingSpendDashboardTokenPublicationSync: Bool {
self.sharedSpendDashboardTokenPublicationDebounceTask != nil
}
#endif

private func scheduleDebouncedTokenPublicationSync() {
self.sharedSpendDashboardTokenPublicationDebounceTask?.cancel()
let delay: Duration = self.startupBehavior.automaticallyStartsBackgroundWork
Expand Down
1 change: 1 addition & 0 deletions Sources/CodexBar/UsageStore+SpendDashboardTokenCost.swift
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ extension UsageStore {
publicationRevision: self.spendDashboardTokenSnapshotPublicationRevision(for: provider),
providerConfigRevision: self.settings.providerConfigRevision(for: provider),
scopeSignature: self.spendDashboardTokenSnapshotScopeSignature(for: provider))
self.synchronizeSharedSpendDashboardAfterTokenPublication(for: provider)
}

private func spendDashboardTokenRefreshPublicationIsCurrent(
Expand Down
Loading