Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
36 changes: 30 additions & 6 deletions Sources/CodexBar/PlanUtilizationHistoryChartMenuView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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<PlanUtilizationSeriesName>?
Expand Down
11 changes: 11 additions & 0 deletions Sources/CodexBar/PlanUtilizationHistoryStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
43 changes: 35 additions & 8 deletions Sources/CodexBar/UsageStore+PlanUtilization.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment on lines +141 to +145

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 Trim merged histories to retention cap

When canonical keys collide, this merge path concatenates and de-duplicates entries but never applies planUtilizationMaxSamples, unlike updatedPlanUtilizationEntries. If a user already has two near-canonical series (for example 299 and 300) each near the retention limit, collapsing them can persist a history far above the intended cap, which increases persistence payload size and processing cost until a later sample happens to trigger trimming. Apply the same retention bound immediately after building merged entries.

Useful? React with 👍 / 👎.

} 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,
Expand All @@ -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
Expand All @@ -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]?
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
26 changes: 26 additions & 0 deletions Tests/CodexBarTests/UsageStorePlanUtilizationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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`() {
Expand Down
Loading