From 68f4bdac54fedda84b469183ccd090d1010e37ab Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 10 Jun 2026 11:05:08 +0100 Subject: [PATCH] perf: reuse hosted chart submenus --- CHANGELOG.md | 1 + .../PlanUtilizationHistoryChartMenuView.swift | 48 +++--- .../PlanUtilizationHistoryStore.swift | 2 +- .../StatusItemController+HostedSubmenus.swift | 152 ++++++++++++++++-- .../CodexBar/StatusItemController+Menu.swift | 5 +- ...ItemController+MenuRefreshScheduling.swift | 2 +- Sources/CodexBar/StatusItemController.swift | 1 + .../CodexBar/UsageStore+PlanUtilization.swift | 8 +- ...UtilizationHistoryChartMenuViewTests.swift | 27 ++++ .../StatusMenuHostedSubmenuRefreshTests.swift | 124 ++++++++++++-- 10 files changed, 321 insertions(+), 49 deletions(-) create mode 100644 Tests/CodexBarTests/PlanUtilizationHistoryChartMenuViewTests.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f92374606..4a31418005 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Antigravity: exclude model quotas without a remaining fraction from family summaries so they no longer mask tracked usage in the automatic menu-bar metric (#1369). Thanks @Martin-Hausleitner! - Claude: add bundled Fable 5 pricing, account for native 1-hour cache-write usage, and refresh Sonnet 4.6 full-context rates (#1368). Thanks @MoollaMore! - Claude: show a direct claude.ai re-login action when a configured web session expires or becomes invalid (#1377). Thanks @LeoLin990405! +- Menu: reuse unchanged hosted chart submenus and precompute utilization history models to reduce expand and hover stalls (#1379). Thanks @hhh2210! - Menu bar: defer data-refresh rebuilds until the tracked menu closes, avoiding multi-second WindowServer stalls with slower providers such as Grok (#1376). Thanks @jangisaac-dev! - Xiaomi MiMo: import automatic session cookies from Safari, Chrome variants, Firefox, and Edge instead of limiting discovery to Chrome (#1304). Thanks @Yuxin-Qiao! diff --git a/Sources/CodexBar/PlanUtilizationHistoryChartMenuView.swift b/Sources/CodexBar/PlanUtilizationHistoryChartMenuView.swift index ae995cc1b4..5a5b36f882 100644 --- a/Sources/CodexBar/PlanUtilizationHistoryChartMenuView.swift +++ b/Sources/CodexBar/PlanUtilizationHistoryChartMenuView.swift @@ -64,8 +64,9 @@ struct PlanUtilizationHistoryChartMenuView: View { } private let provider: UsageProvider - private let histories: [PlanUtilizationSeriesHistory] - private let snapshot: UsageSnapshot? + private let visibleSeries: [VisibleSeries] + private let modelsBySeriesID: [String: Model] + private let emptyModel: Model private let width: CGFloat @State private var selectedSeriesID: String? @@ -78,32 +79,33 @@ struct PlanUtilizationHistoryChartMenuView: View { width: CGFloat) { self.provider = provider - self.histories = histories - self.snapshot = snapshot + let visibleSeries = Self.visibleSeries( + histories: histories, + provider: provider, + snapshot: snapshot) + let referenceDate = Date() + self.visibleSeries = visibleSeries + self.modelsBySeriesID = Dictionary(uniqueKeysWithValues: visibleSeries.map { + ($0.id, Self.makeModel(history: $0.history, provider: provider, referenceDate: referenceDate)) + }) + self.emptyModel = Self.emptyModel(provider: provider) self.width = width } var body: some View { - let visibleSeries = Self.visibleSeries( - histories: self.histories, - provider: self.provider, - snapshot: self.snapshot) - let effectiveSelectedSeries = visibleSeries.first(where: { $0.id == self.selectedSeriesID }) ?? visibleSeries - .first - let model = Self.makeModel( - history: effectiveSelectedSeries?.history, - provider: self.provider, - referenceDate: Date()) + let effectiveSelectedSeries = self.visibleSeries.first(where: { $0.id == self.selectedSeriesID }) + ?? self.visibleSeries.first + let model = effectiveSelectedSeries.flatMap { self.modelsBySeriesID[$0.id] } ?? self.emptyModel VStack(alignment: .leading, spacing: 10) { - if visibleSeries.count > 1 { + if self.visibleSeries.count > 1 { Picker(selection: Binding( get: { effectiveSelectedSeries?.id ?? "" }, set: { newValue in self.selectedSeriesID = newValue self.selectedPointID = nil })) { - ForEach(visibleSeries) { series in + ForEach(self.visibleSeries) { series in Text(series.title).tag(series.id) } } label: { @@ -172,9 +174,9 @@ struct PlanUtilizationHistoryChartMenuView: View { .padding(.horizontal, 16) .padding(.vertical, 10) .frame(minWidth: self.width, maxWidth: .infinity, alignment: .topLeading) - .task(id: visibleSeries.map(\.id).joined(separator: ",")) { - guard let firstVisibleSeries = visibleSeries.first else { return } - guard !visibleSeries.contains(where: { $0.id == self.selectedSeriesID }) else { return } + .task(id: self.visibleSeries.map(\.id).joined(separator: ",")) { + guard let firstVisibleSeries = self.visibleSeries.first else { return } + guard !self.visibleSeries.contains(where: { $0.id == self.selectedSeriesID }) else { return } self.selectedSeriesID = firstVisibleSeries.id self.selectedPointID = nil } @@ -228,12 +230,12 @@ struct PlanUtilizationHistoryChartMenuView: View { } } - private nonisolated static func mergedEntries( + nonisolated static func mergedEntries( _ entries: [PlanUtilizationHistoryEntry]) -> [PlanUtilizationHistoryEntry] { - entries.reduce(into: []) { result, entry in - guard !result.contains(entry) else { return } - result.append(entry) + var seen: Set = [] + return entries.filter { entry in + seen.insert(entry).inserted } } diff --git a/Sources/CodexBar/PlanUtilizationHistoryStore.swift b/Sources/CodexBar/PlanUtilizationHistoryStore.swift index 13b6b4d970..e9843f309e 100644 --- a/Sources/CodexBar/PlanUtilizationHistoryStore.swift +++ b/Sources/CodexBar/PlanUtilizationHistoryStore.swift @@ -28,7 +28,7 @@ struct PlanUtilizationSeriesName: RawRepresentable, Hashable, Codable, Expressib } } -struct PlanUtilizationHistoryEntry: Codable, Equatable { +struct PlanUtilizationHistoryEntry: Codable, Equatable, Hashable { let capturedAt: Date let usedPercent: Double let resetsAt: Date? diff --git a/Sources/CodexBar/StatusItemController+HostedSubmenus.swift b/Sources/CodexBar/StatusItemController+HostedSubmenus.swift index b4c7bfad2d..71b1d4170a 100644 --- a/Sources/CodexBar/StatusItemController+HostedSubmenus.swift +++ b/Sources/CodexBar/StatusItemController+HostedSubmenus.swift @@ -3,6 +3,12 @@ import CodexBarCore import SwiftUI extension StatusItemController { + private struct HostedSubviewIdentity { + let chartID: String + let provider: UsageProvider? + let providerRawValue: String? + } + func isHostedSubviewMenu(_ menu: NSMenu) -> Bool { let ids: Set = [ Self.usageBreakdownChartID, @@ -37,16 +43,21 @@ extension StatusItemController { return submenu } - func hydrateHostedSubviewMenuIfNeeded(_ menu: NSMenu, width requestedWidth: CGFloat? = nil) { + @discardableResult + func hydrateHostedSubviewMenuIfNeeded(_ menu: NSMenu, width requestedWidth: CGFloat? = nil) -> Bool { guard let placeholder = menu.items.first, menu.items.count == 1, placeholder.view == nil, let chartID = placeholder.representedObject as? String else { - return + return false } let width = requestedWidth ?? self.renderedMenuWidth(for: menu.supermenu ?? menu) + let identity = HostedSubviewIdentity( + chartID: chartID, + provider: placeholder.toolTip.flatMap(UsageProvider.init(rawValue:)), + providerRawValue: placeholder.toolTip) menu.removeAllItems() let didHydrate: Bool = switch chartID { @@ -90,8 +101,14 @@ extension StatusItemController { false } - guard !didHydrate else { return } - self.appendHostedSubviewUnavailableItem(to: menu, chartID: chartID, providerRawValue: placeholder.toolTip) + if !didHydrate { + self.appendHostedSubviewUnavailableItem( + to: menu, + chartID: chartID, + providerRawValue: placeholder.toolTip) + } + self.recordHostedSubviewRenderSignature(for: menu, identity: identity, width: width) + return true } func refreshHostedSubviewMenu(_ menu: NSMenu) { @@ -100,6 +117,13 @@ extension StatusItemController { self.refreshHostedSubviewHeights(in: menu) return } + let signature = self.hostedSubviewRenderSignature(identity: identity, width: width) + if self.hostedSubviewRenderSignatures.object(forKey: menu) as String? == signature { + if identity.chartID == Self.zaiHourlyUsageChartID { + self.refreshHostedSubviewHeights(in: menu) + } + return + } menu.removeAllItems() let didHydrate: Bool = switch identity.chartID { @@ -135,22 +159,21 @@ extension StatusItemController { false } - if didHydrate { - self.refreshHostedSubviewHeights(in: menu) - } else { + if !didHydrate { self.appendHostedSubviewUnavailableItem( to: menu, chartID: identity.chartID, providerRawValue: identity.provider?.rawValue ?? identity.providerRawValue) } + self.hostedSubviewRenderSignatures.setObject(signature as NSString, forKey: menu) } private func hostedSubviewIdentity(for menu: NSMenu) - -> (chartID: String, provider: UsageProvider?, providerRawValue: String?)? { + -> HostedSubviewIdentity? { for item in menu.items { guard let chartID = item.representedObject as? String else { continue } let providerRawValue = item.toolTip - return ( + return HostedSubviewIdentity( chartID: chartID, provider: providerRawValue.flatMap(UsageProvider.init(rawValue:)), providerRawValue: providerRawValue) @@ -158,6 +181,117 @@ extension StatusItemController { return nil } + private func recordHostedSubviewRenderSignature( + for menu: NSMenu, + identity: HostedSubviewIdentity, + width: CGFloat) + { + let signature = self.hostedSubviewRenderSignature(identity: identity, width: width) + self.hostedSubviewRenderSignatures.setObject(signature as NSString, forKey: menu) + } + + private func hostedSubviewRenderSignature( + identity: HostedSubviewIdentity, + width: CGFloat) -> String + { + let contentSignature: String = switch identity.chartID { + case Self.usageBreakdownChartID: + Self.dashboardBreakdownReadinessSignature( + OpenAIDashboardDailyBreakdown.removingSkillUsageServices( + from: self.store.openAIDashboard?.usageBreakdown ?? [])) + case Self.creditsHistoryChartID: + Self.dashboardBreakdownReadinessSignature(self.store.openAIDashboard?.dailyBreakdown ?? []) + case Self.costHistoryChartID: + identity.provider.map(self.costHistoryRenderSignature(for:)) ?? "missing-provider" + case Self.usageHistoryChartID: + identity.provider.map(self.usageHistoryRenderSignature(for:)) ?? "missing-provider" + case Self.storageBreakdownID: + identity.provider.map(self.storageBreakdownRenderSignature(for:)) ?? "missing-provider" + case Self.zaiHourlyUsageChartID: + identity.provider.map(self.zaiHourlyUsageRenderSignature(for:)) ?? "missing-provider" + default: + "unknown" + } + return [ + identity.chartID, + identity.providerRawValue ?? "", + String(Double(width).bitPattern, radix: 16), + contentSignature, + ].joined(separator: "|") + } + + private func costHistoryRenderSignature(for provider: UsageProvider) -> String { + guard let snapshot = self.tokenSnapshotForCostHistorySubmenu(provider: provider) else { return "none" } + return [ + snapshot.currencyCode, + "\(snapshot.historyDays)", + snapshot.historyLabel ?? "", + snapshot.last30DaysCostUSD.map { String($0.bitPattern, radix: 16) } ?? "nil", + String(reflecting: snapshot.daily), + ].joined(separator: "|") + } + + private func usageHistoryRenderSignature(for provider: UsageProvider) -> String { + let snapshot = self.store.snapshot(for: provider) + let selection = self.store.planUtilizationHistorySelection(for: provider) + return [ + "\(self.store.planUtilizationHistoryRevision)", + "\(Int(Date().timeIntervalSince1970 / 60))", + selection.accountKey ?? "unscoped", + snapshot?.primary == nil ? "0" : "1", + snapshot?.secondary == nil ? "0" : "1", + snapshot?.tertiary == nil ? "0" : "1", + ].joined(separator: "|") + } + + private func storageBreakdownRenderSignature(for provider: UsageProvider) -> String { + guard let footprint = self.store.storageFootprint(for: provider) else { return "none" } + let components = footprint.components + .map { "\($0.path)=\($0.totalBytes)" } + .joined(separator: ";") + return [ + "\(footprint.totalBytes)", + footprint.paths.joined(separator: ";"), + footprint.missingPaths.joined(separator: ";"), + footprint.unreadablePaths.joined(separator: ";"), + components, + String(Double(self.storageBreakdownMenuMaxHeight()).bitPattern, radix: 16), + ].joined(separator: "|") + } + + private func zaiHourlyUsageRenderSignature(for provider: UsageProvider) -> String { + guard let modelUsage = self.store.snapshot(for: provider)?.zaiUsage?.modelUsage else { return "none" } + return Self.zaiHourlyUsageRenderSignature(modelUsage: modelUsage, now: Date()) + } + + static func zaiHourlyUsageRenderSignature(modelUsage: ZaiModelUsageData, now: Date) -> String { + let models = modelUsage.modelDataList + .map { model in + let usage = model.tokensUsage + .map { $0.map(String.init) ?? "nil" } + .joined(separator: ",") + return "\(model.modelName ?? "")=\(usage)" + } + .joined(separator: ";") + let ranges: [ZaiHourlyRange] = [.today(referenceDate: now), .last24h] + let visibleBars = ranges + .map { range in + ZaiHourlyBars.from(modelData: modelUsage, range: range, now: now) + .map { bar in + let segments = bar.segments + .map { "\($0.model)=\($0.tokens)" } + .joined(separator: ",") + return "\(bar.label):\(segments)" + } + .joined(separator: ";") + } + return [ + modelUsage.xTime.joined(separator: ","), + models, + visibleBars.joined(separator: "|"), + ].joined(separator: "|") + } + private func appendHostedSubviewUnavailableItem( to menu: NSMenu, chartID: String, diff --git a/Sources/CodexBar/StatusItemController+Menu.swift b/Sources/CodexBar/StatusItemController+Menu.swift index db4b49abcd..c0e254bb37 100644 --- a/Sources/CodexBar/StatusItemController+Menu.swift +++ b/Sources/CodexBar/StatusItemController+Menu.swift @@ -84,8 +84,9 @@ extension StatusItemController { let menuTrackingWasIdle = self.openMenus.isEmpty if self.isHostedSubviewMenu(menu) { - self.hydrateHostedSubviewMenuIfNeeded(menu) - self.refreshHostedSubviewHeights(in: menu) + if !self.hydrateHostedSubviewMenuIfNeeded(menu) { + self.refreshHostedSubviewMenu(menu) + } if self.isMenuRefreshEnabled, self.isOpenAIWebSubviewMenu(menu) { self.deferOpenAIDashboardRefreshUntilMenuCloses(reason: "submenu open") } diff --git a/Sources/CodexBar/StatusItemController+MenuRefreshScheduling.swift b/Sources/CodexBar/StatusItemController+MenuRefreshScheduling.swift index f9aab228da..4980a019be 100644 --- a/Sources/CodexBar/StatusItemController+MenuRefreshScheduling.swift +++ b/Sources/CodexBar/StatusItemController+MenuRefreshScheduling.swift @@ -127,7 +127,7 @@ extension StatusItemController { return parts.joined(separator: "|") } - private static func dashboardBreakdownReadinessSignature( + static func dashboardBreakdownReadinessSignature( _ breakdown: [OpenAIDashboardDailyBreakdown]) -> String { breakdown diff --git a/Sources/CodexBar/StatusItemController.swift b/Sources/CodexBar/StatusItemController.swift index dd246f1a5e..7d28152d6d 100644 --- a/Sources/CodexBar/StatusItemController.swift +++ b/Sources/CodexBar/StatusItemController.swift @@ -118,6 +118,7 @@ final class StatusItemController: NSObject, NSMenuDelegate, StatusItemControllin var latestRequiredMenuRebuildVersion: Int = 0 var menuVersions: [ObjectIdentifier: Int] = [:] var menuReadinessSignatures: [ObjectIdentifier: String] = [:] + let hostedSubviewRenderSignatures = NSMapTable.weakToStrongObjects() var menuCardHeightCache: [MenuCardHeightCacheKey: CGFloat] = [:] var measuredStandardMenuWidthCache: [String: CGFloat] = [:] var lastMenuAdjunctReadinessSignature = "" diff --git a/Sources/CodexBar/UsageStore+PlanUtilization.swift b/Sources/CodexBar/UsageStore+PlanUtilization.swift index ad01e74604..29406373ac 100644 --- a/Sources/CodexBar/UsageStore+PlanUtilization.swift +++ b/Sources/CodexBar/UsageStore+PlanUtilization.swift @@ -36,6 +36,12 @@ extension UsageStore { } func planUtilizationHistory(for provider: UsageProvider) -> [PlanUtilizationSeriesHistory] { + self.planUtilizationHistorySelection(for: provider).histories + } + + func planUtilizationHistorySelection(for provider: UsageProvider) + -> (accountKey: String?, histories: [PlanUtilizationSeriesHistory]) + { var providerBuckets = self.planUtilizationHistory[provider] ?? PlanUtilizationHistoryBuckets() let originalProviderBuckets = providerBuckets let accountKey = self.resolvePlanUtilizationAccountKey( @@ -50,7 +56,7 @@ extension UsageStore { await self.planUtilizationPersistenceCoordinator.enqueue(snapshotToPersist) } } - return providerBuckets.histories(for: accountKey) + return (accountKey, providerBuckets.histories(for: accountKey)) } func codexPlanUtilizationHistories(forVisibleAccount account: CodexVisibleAccount) diff --git a/Tests/CodexBarTests/PlanUtilizationHistoryChartMenuViewTests.swift b/Tests/CodexBarTests/PlanUtilizationHistoryChartMenuViewTests.swift new file mode 100644 index 0000000000..3d41598dc0 --- /dev/null +++ b/Tests/CodexBarTests/PlanUtilizationHistoryChartMenuViewTests.swift @@ -0,0 +1,27 @@ +import Foundation +import Testing +@testable import CodexBar + +@Suite +struct PlanUtilizationHistoryChartMenuViewTests { + @Test + func `merged entries preserve first occurrence order while removing duplicates`() { + let first = PlanUtilizationHistoryEntry( + capturedAt: Date(timeIntervalSince1970: 100), + usedPercent: 10, + resetsAt: Date(timeIntervalSince1970: 200)) + let second = PlanUtilizationHistoryEntry( + capturedAt: Date(timeIntervalSince1970: 300), + usedPercent: 20, + resetsAt: nil) + + let merged = PlanUtilizationHistoryChartMenuView.mergedEntries([ + first, + second, + first, + second, + ]) + + #expect(merged == [first, second]) + } +} diff --git a/Tests/CodexBarTests/StatusMenuHostedSubmenuRefreshTests.swift b/Tests/CodexBarTests/StatusMenuHostedSubmenuRefreshTests.swift index 2a6323058b..86ad13c189 100644 --- a/Tests/CodexBarTests/StatusMenuHostedSubmenuRefreshTests.swift +++ b/Tests/CodexBarTests/StatusMenuHostedSubmenuRefreshTests.swift @@ -196,6 +196,81 @@ struct StatusMenuHostedSubmenuRefreshTests { } } + @Test + func `zai chart render signature follows time range boundaries`() throws { + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM-dd HH:mm" + formatter.locale = Locale(identifier: "en_US_POSIX") + let beforeMidnight = try #require(formatter.date(from: "2026-01-01 23:30")) + let afterMidnight = try #require(formatter.date(from: "2026-01-02 00:30")) + let modelUsage = ZaiModelUsageData( + xTime: ["2026-01-01 23:00"], + modelDataList: [ + ZaiModelDataItem(modelName: "glm-4.5", tokensUsage: [100]), + ]) + + let before = StatusItemController.zaiHourlyUsageRenderSignature( + modelUsage: modelUsage, + now: beforeMidnight) + let after = StatusItemController.zaiHourlyUsageRenderSignature( + modelUsage: modelUsage, + now: afterMidnight) + + #expect(before != after) + } + + @Test + func `utilization chart invalidates when active account changes`() throws { + let previousMenuCardRendering = StatusItemController.menuCardRenderingEnabled + StatusItemController.menuCardRenderingEnabled = true + defer { StatusItemController.menuCardRenderingEnabled = previousMenuCardRendering } + + let settings = Self.makeSettings() + settings.statusChecksEnabled = false + settings.refreshFrequency = .manual + Self.enableOnlyClaude(settings) + settings.addTokenAccount(provider: .claude, label: "Alice", token: "alice-token") + settings.addTokenAccount(provider: .claude, label: "Bob", token: "bob-token") + let accounts = settings.tokenAccounts(for: .claude) + let alice = try #require(accounts.first) + let bob = try #require(accounts.last) + let aliceKey = try #require( + UsageStore._planUtilizationTokenAccountKeyForTesting(provider: .claude, account: alice)) + let bobKey = try #require( + UsageStore._planUtilizationTokenAccountKeyForTesting(provider: .claude, account: bob)) + + let fetcher = UsageFetcher() + let store = UsageStore(fetcher: fetcher, browserDetection: BrowserDetection(cacheTTL: 0), settings: settings) + Self.seedClaudeSnapshots(in: store) + store.planUtilizationHistory[.claude] = PlanUtilizationHistoryBuckets(accounts: [ + aliceKey: [Self.makePlanHistory(usedPercent: 20)], + bobKey: [Self.makePlanHistory(usedPercent: 50)], + ]) + settings.setActiveTokenAccountIndex(0, for: .claude) + + let controller = StatusItemController( + store: store, + settings: settings, + account: fetcher.loadAccountInfo(), + updater: DisabledUpdaterController(), + preferencesSelection: PreferencesSelection(), + statusBar: .system) + defer { controller.releaseStatusItemsForTesting() } + + let submenu = controller.makeHostedSubviewPlaceholderMenu( + chartID: StatusItemController.usageHistoryChartID, + provider: .claude, + width: StatusItemController.menuCardBaseWidth) + controller.menuWillOpen(submenu) + let aliceView = try #require(submenu.items.first?.view) + + settings.setActiveTokenAccountIndex(1, for: .claude) + controller.refreshHostedSubviewMenu(submenu) + + let bobView = try #require(submenu.items.first?.view) + #expect(bobView !== aliceView) + } + private func assertHostedChartItemHeightMatchesRefresh( chartID: String, provider: UsageProvider, @@ -286,6 +361,14 @@ struct StatusMenuHostedSubmenuRefreshTests { #expect(hydratedItem.toolTip == provider.rawValue) #expect(hydratedItem.view != nil) #expect(hydratedItem.title != "No data available") + let hydratedView = hydratedItem.view + let inflatedHeight = hydratedView.map { view -> CGFloat in + let inflatedHeight = view.frame.height + 100 + if chartID == StatusItemController.zaiHourlyUsageChartID { + view.frame.size.height = inflatedHeight + } + return inflatedHeight + } controller.refreshHostedSubviewMenu(submenu) @@ -294,6 +377,19 @@ struct StatusMenuHostedSubmenuRefreshTests { #expect(refreshedItem.toolTip == provider.rawValue) #expect(refreshedItem.view != nil) #expect(refreshedItem.title != "No data available") + #expect(refreshedItem.view === hydratedView) + if chartID == StatusItemController.zaiHourlyUsageChartID { + #expect(refreshedItem.view?.frame.height != inflatedHeight) + } + + if chartID == StatusItemController.costHistoryChartID, provider == .claude { + store._setTokenSnapshotForTesting(Self.makeTokenSnapshot(dailyCost: 2.34), provider: .claude) + controller.refreshHostedSubviewMenu(submenu) + + let changedItem = try #require(submenu.items.first) + #expect(changedItem.view != nil) + #expect(changedItem.view !== hydratedView) + } } private static func makeSettings() -> SettingsStore { @@ -370,15 +466,19 @@ struct StatusMenuHostedSubmenuRefreshTests { self.seedClaudeSnapshots(in: store) store.planUtilizationHistory[.claude] = PlanUtilizationHistoryBuckets( unscoped: [ - PlanUtilizationSeriesHistory( - name: .session, - windowMinutes: 300, - entries: [ - PlanUtilizationHistoryEntry( - capturedAt: Date(timeIntervalSince1970: 1_700_000_000), - usedPercent: 24, - resetsAt: Date(timeIntervalSince1970: 1_700_018_000)), - ]), + self.makePlanHistory(usedPercent: 24), + ]) + } + + private static func makePlanHistory(usedPercent: Double) -> PlanUtilizationSeriesHistory { + PlanUtilizationSeriesHistory( + name: .session, + windowMinutes: 300, + entries: [ + PlanUtilizationHistoryEntry( + capturedAt: Date(timeIntervalSince1970: 1_700_000_000), + usedPercent: usedPercent, + resetsAt: Date(timeIntervalSince1970: 1_700_018_000)), ]) } @@ -419,19 +519,19 @@ struct StatusMenuHostedSubmenuRefreshTests { store._setSnapshotForTesting(snapshot, provider: .zai) } - private static func makeTokenSnapshot() -> CostUsageTokenSnapshot { + private static func makeTokenSnapshot(dailyCost: Double = 1.23) -> CostUsageTokenSnapshot { CostUsageTokenSnapshot( sessionTokens: 123, sessionCostUSD: 0.12, last30DaysTokens: 123, - last30DaysCostUSD: 1.23, + last30DaysCostUSD: dailyCost, daily: [ CostUsageDailyReport.Entry( date: "2025-12-23", inputTokens: nil, outputTokens: nil, totalTokens: 123, - costUSD: 1.23, + costUSD: dailyCost, modelsUsed: nil, modelBreakdowns: nil), ],