From 98f50abdd6ecee1f431d8533cee8eb0c41c8dcca Mon Sep 17 00:00:00 2001 From: Nimrod Gutman Date: Mon, 18 May 2026 15:07:14 +0300 Subject: [PATCH 1/2] fix(ui): collapse duplicate utilization tabs --- .../PlanUtilizationHistoryChartMenuView.swift | 36 +++++++++++++--- .../PlanUtilizationHistoryStore.swift | 11 +++++ .../CodexBar/UsageStore+PlanUtilization.swift | 43 +++++++++++++++---- ...ePlanUtilizationResetCoalescingTests.swift | 30 +++++++++++++ .../UsageStorePlanUtilizationTests.swift | 26 +++++++++++ 5 files changed, 132 insertions(+), 14 deletions(-) diff --git a/Sources/CodexBar/PlanUtilizationHistoryChartMenuView.swift b/Sources/CodexBar/PlanUtilizationHistoryChartMenuView.swift index 072c10cc1d9..3556058bde8 100644 --- a/Sources/CodexBar/PlanUtilizationHistoryChartMenuView.swift +++ b/Sources/CodexBar/PlanUtilizationHistoryChartMenuView.swift @@ -184,13 +184,28 @@ struct PlanUtilizationHistoryChartMenuView: View { { let metadata = ProviderDescriptorRegistry.metadata[provider] let allowedNames = self.visibleSeriesNames(provider: provider, snapshot: snapshot) - return histories - .filter { history in - guard !history.entries.isEmpty else { return false } - guard history.windowMinutes > 0 else { return false } - guard let allowedNames else { return true } - return allowedNames.contains(history.name) + var historiesBySelection: [SeriesSelection: PlanUtilizationSeriesHistory] = [:] + for history in histories { + guard !history.entries.isEmpty else { continue } + guard history.windowMinutes > 0 else { continue } + guard allowedNames?.contains(history.name) ?? true else { continue } + + let canonicalWindowMinutes = history.name.canonicalWindowMinutes(history.windowMinutes) + let selection = SeriesSelection(name: history.name, windowMinutes: canonicalWindowMinutes) + if let existingHistory = historiesBySelection[selection] { + historiesBySelection[selection] = PlanUtilizationSeriesHistory( + name: history.name, + windowMinutes: canonicalWindowMinutes, + entries: Self.mergedEntries(existingHistory.entries + history.entries)) + } else { + historiesBySelection[selection] = PlanUtilizationSeriesHistory( + name: history.name, + windowMinutes: canonicalWindowMinutes, + entries: history.entries) } + } + + return historiesBySelection.values .sorted { lhs, rhs in let lhsOrder = self.seriesSortOrder(lhs.name) let rhsOrder = self.seriesSortOrder(rhs.name) @@ -210,6 +225,15 @@ struct PlanUtilizationHistoryChartMenuView: View { } } + private nonisolated static func mergedEntries( + _ entries: [PlanUtilizationHistoryEntry]) -> [PlanUtilizationHistoryEntry] + { + entries.reduce(into: []) { result, entry in + guard !result.contains(entry) else { return } + result.append(entry) + } + } + private nonisolated static func visibleSeriesNames( provider: UsageProvider, snapshot: UsageSnapshot?) -> Set? diff --git a/Sources/CodexBar/PlanUtilizationHistoryStore.swift b/Sources/CodexBar/PlanUtilizationHistoryStore.swift index 9bb5f6bc7ef..13b6b4d970b 100644 --- a/Sources/CodexBar/PlanUtilizationHistoryStore.swift +++ b/Sources/CodexBar/PlanUtilizationHistoryStore.swift @@ -15,6 +15,17 @@ struct PlanUtilizationSeriesName: RawRepresentable, Hashable, Codable, Expressib static let session: Self = "session" static let weekly: Self = "weekly" static let opus: Self = "opus" + + func canonicalWindowMinutes(_ windowMinutes: Int) -> Int { + switch self { + case .session where (295...305).contains(windowMinutes): + 300 + case .weekly where (10070...10090).contains(windowMinutes): + 10080 + default: + windowMinutes + } + } } struct PlanUtilizationHistoryEntry: Codable, Equatable { diff --git a/Sources/CodexBar/UsageStore+PlanUtilization.swift b/Sources/CodexBar/UsageStore+PlanUtilization.swift index a9b130e646e..82b81708b54 100644 --- a/Sources/CodexBar/UsageStore+PlanUtilization.swift +++ b/Sources/CodexBar/UsageStore+PlanUtilization.swift @@ -128,13 +128,30 @@ extension UsageStore { { guard !samples.isEmpty else { return nil } - var historiesByKey = Dictionary(uniqueKeysWithValues: existingHistories.map { - (PlanUtilizationSeriesKey(name: $0.name, windowMinutes: $0.windowMinutes), $0) - }) + var historiesByKey: [PlanUtilizationSeriesKey: PlanUtilizationSeriesHistory] = [:] var didChange = false + for history in existingHistories { + let canonicalWindowMinutes = history.name.canonicalWindowMinutes(history.windowMinutes) + let key = PlanUtilizationSeriesKey(name: history.name, windowMinutes: canonicalWindowMinutes) + let canonicalHistory = PlanUtilizationSeriesHistory( + name: history.name, + windowMinutes: canonicalWindowMinutes, + entries: history.entries) + if let existingHistory = historiesByKey[key] { + historiesByKey[key] = PlanUtilizationSeriesHistory( + name: history.name, + windowMinutes: canonicalWindowMinutes, + entries: self.mergedPlanUtilizationEntries(existingHistory.entries + canonicalHistory.entries)) + didChange = true + } else { + historiesByKey[key] = canonicalHistory + didChange = didChange || canonicalWindowMinutes != history.windowMinutes + } + } for sample in samples { - let key = PlanUtilizationSeriesKey(name: sample.name, windowMinutes: sample.windowMinutes) + let canonicalWindowMinutes = sample.name.canonicalWindowMinutes(sample.windowMinutes) + let key = PlanUtilizationSeriesKey(name: sample.name, windowMinutes: canonicalWindowMinutes) if let existingHistory = historiesByKey[key] { guard let updatedEntries = self.updatedPlanUtilizationEntries( existingEntries: existingHistory.entries, @@ -144,12 +161,12 @@ extension UsageStore { } historiesByKey[key] = PlanUtilizationSeriesHistory( name: sample.name, - windowMinutes: sample.windowMinutes, + windowMinutes: canonicalWindowMinutes, entries: updatedEntries) } else { historiesByKey[key] = PlanUtilizationSeriesHistory( name: sample.name, - windowMinutes: sample.windowMinutes, + windowMinutes: canonicalWindowMinutes, entries: [sample.entry]) } didChange = true @@ -164,6 +181,15 @@ extension UsageStore { } } + private nonisolated static func mergedPlanUtilizationEntries( + _ entries: [PlanUtilizationHistoryEntry]) -> [PlanUtilizationHistoryEntry] + { + entries.reduce(into: []) { result, entry in + guard !result.contains(entry) else { return } + result.append(entry) + } + } + private nonisolated static func updatedPlanUtilizationEntries( existingEntries: [PlanUtilizationHistoryEntry], entry: PlanUtilizationHistoryEntry) -> [PlanUtilizationHistoryEntry]? @@ -292,10 +318,11 @@ extension UsageStore { return } - let key = PlanUtilizationSeriesKey(name: name, windowMinutes: windowMinutes) + let canonicalWindowMinutes = name.canonicalWindowMinutes(windowMinutes) + let key = PlanUtilizationSeriesKey(name: name, windowMinutes: canonicalWindowMinutes) samplesByKey[key] = PlanUtilizationSeriesSample( name: name, - windowMinutes: windowMinutes, + windowMinutes: canonicalWindowMinutes, entry: PlanUtilizationHistoryEntry( capturedAt: capturedAt, usedPercent: usedPercent, diff --git a/Tests/CodexBarTests/UsageStorePlanUtilizationResetCoalescingTests.swift b/Tests/CodexBarTests/UsageStorePlanUtilizationResetCoalescingTests.swift index a0df15b3d80..076f20b5f05 100644 --- a/Tests/CodexBarTests/UsageStorePlanUtilizationResetCoalescingTests.swift +++ b/Tests/CodexBarTests/UsageStorePlanUtilizationResetCoalescingTests.swift @@ -4,6 +4,36 @@ import Testing @testable import CodexBar struct UsageStorePlanUtilizationResetCoalescingTests { + @Test + func `near canonical codex windows merge into canonical history series`() throws { + let base = Date(timeIntervalSince1970: 1_700_000_000) + let existing = [ + planSeries(name: .session, windowMinutes: 300, entries: [ + planEntry(at: base, usedPercent: 20), + ]), + planSeries(name: .weekly, windowMinutes: 10080, entries: [ + planEntry(at: base, usedPercent: 40), + ]), + ] + let incoming = [ + planSeries(name: .session, windowMinutes: 299, entries: [ + planEntry(at: base.addingTimeInterval(3600), usedPercent: 30), + ]), + planSeries(name: .weekly, windowMinutes: 10079, entries: [ + planEntry(at: base.addingTimeInterval(3600), usedPercent: 50), + ]), + ] + + let updated = try #require( + UsageStore._updatedPlanUtilizationHistoriesForTesting( + existingHistories: existing, + samples: incoming)) + + #expect(updated.map { "\($0.name.rawValue):\($0.windowMinutes)" } == ["session:300", "weekly:10080"]) + #expect(findSeries(updated, name: .session, windowMinutes: 300)?.entries.map(\.usedPercent) == [20, 30]) + #expect(findSeries(updated, name: .weekly, windowMinutes: 10080)?.entries.map(\.usedPercent) == [40, 50]) + } + @Test func `same hour entry backfills missing reset metadata`() throws { let calendar = Calendar(identifier: .gregorian) diff --git a/Tests/CodexBarTests/UsageStorePlanUtilizationTests.swift b/Tests/CodexBarTests/UsageStorePlanUtilizationTests.swift index e05e67f2f46..53070c75c1d 100644 --- a/Tests/CodexBarTests/UsageStorePlanUtilizationTests.swift +++ b/Tests/CodexBarTests/UsageStorePlanUtilizationTests.swift @@ -142,6 +142,32 @@ struct UsageStorePlanUtilizationTests { #expect(model.selectedSeries == "session:300") } + @MainActor + @Test + func `native chart folds near canonical codex windows into single tabs`() { + let histories = [ + planSeries(name: .session, windowMinutes: 299, entries: [ + planEntry(at: Date(timeIntervalSince1970: 1_700_000_000), usedPercent: 20), + ]), + planSeries(name: .session, windowMinutes: 300, entries: [ + planEntry(at: Date(timeIntervalSince1970: 1_700_003_600), usedPercent: 30), + ]), + planSeries(name: .weekly, windowMinutes: 10079, entries: [ + planEntry(at: Date(timeIntervalSince1970: 1_700_086_400), usedPercent: 40), + ]), + planSeries(name: .weekly, windowMinutes: 10080, entries: [ + planEntry(at: Date(timeIntervalSince1970: 1_700_172_800), usedPercent: 50), + ]), + ] + + let model = PlanUtilizationHistoryChartMenuView._modelSnapshotForTesting( + histories: histories, + provider: .codex) + + #expect(model.visibleSeries == ["session:300", "weekly:10080"]) + #expect(model.selectedSeries == "session:300") + } + @MainActor @Test func `claude history tabs match current snapshot bars`() { From acb5937d3acb09f3e3177d037a8106fad9c7e4af Mon Sep 17 00:00:00 2001 From: Nimrod Gutman Date: Mon, 18 May 2026 15:08:55 +0300 Subject: [PATCH 2/2] docs(changelog): note duplicate utilization tab fix --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 894f897d64d..27a50e533b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - Permissions: notify when a provider probe detects a macOS/browser permission prompt waiting for user action (#456). ### Fixed +- Codex: collapse near-duplicate session and weekly plan-utilization history windows so charts no longer show repeated tabs (#1027). Thanks @ngutman! - Multi-account menus: fetch stacked Codex/token-account usage concurrently so account switchers stay responsive with many accounts (#1011). ## 0.27.0 — 2026-05-17