From 472f90d1d965f778aff11c84f3c5fe035b2e98dc Mon Sep 17 00:00:00 2001 From: Yuxin Qiao <104957188+Yuxin-Qiao@users.noreply.github.com> Date: Fri, 22 May 2026 16:19:50 +0800 Subject: [PATCH 1/4] Display: add workday segmentation for weekly progress bars MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #1096 — Weekly Progress Bar configurable to segment in number of work days. - Add a Display preference for weekly progress work-day segmentation: Off / 4 days / 5 days / 7 days - Draw display-only day-boundary tick marks on weekly usage bars - Keep the default Off / nil so existing users see no UI change - Merge work-day markers with existing quota warning markers - No warning/alert behavior added; future warnings remain out of scope for this PR Validation: - swift test --filter MenuCardQuotaWarningMarkerTests - swift test --filter SettingsStoreCoverageTests - swift test --filter MenuCardModelTests - make check --- .../MenuCardQuotaWarningMarkers.swift | 9 ++++ Sources/CodexBar/MenuCardView.swift | 9 +++- Sources/CodexBar/PreferencesDisplayPane.swift | 19 +++++++++ .../CodexBar/PreferencesProvidersPane.swift | 1 + .../Resources/en.lproj/Localizable.strings | 2 + Sources/CodexBar/SettingsStore+Defaults.swift | 12 ++++++ Sources/CodexBar/SettingsStore.swift | 2 + Sources/CodexBar/SettingsStoreState.swift | 1 + .../StatusItemController+MenuCardModel.swift | 1 + .../MenuCardQuotaWarningMarkerTests.swift | 42 +++++++++++++++++++ .../SettingsStoreCoverageTests.swift | 32 ++++++++++++++ 11 files changed, 128 insertions(+), 2 deletions(-) diff --git a/Sources/CodexBar/MenuCardQuotaWarningMarkers.swift b/Sources/CodexBar/MenuCardQuotaWarningMarkers.swift index 89505ac5d..c5f0c1852 100644 --- a/Sources/CodexBar/MenuCardQuotaWarningMarkers.swift +++ b/Sources/CodexBar/MenuCardQuotaWarningMarkers.swift @@ -19,3 +19,12 @@ extension UsageMenuCardView.Model { .filter { $0 > 0 && $0 < 100 } } } + +/// Returns boundary percentages for work day markers on a weekly progress bar. +/// Only valid when windowMinutes == 10080 (standard 7-day week). +/// nil workDays means feature is disabled. +func workDayMarkerPercents(workDays: Int?, windowMinutes: Int?) -> [Double] { + guard workDays != nil, windowMinutes == 10080 else { return [] } + guard let wd = workDays, wd >= 2, wd <= 7 else { return [] } + return (1.. SettingsStore { let defaults = UserDefaults(suiteName: suiteName)! defaults.removePersistentDomain(forName: suiteName) From d8aa68866e42b8d18e4d87aacf4fbbb82a3daf6a Mon Sep 17 00:00:00 2001 From: Yuxin Qiao <104957188+Yuxin-Qiao@users.noreply.github.com> Date: Fri, 22 May 2026 19:08:09 +0800 Subject: [PATCH 2/4] Fix weekly workday marker observation and codex lanes --- .../MenuCardQuotaWarningMarkers.swift | 50 +++++ Sources/CodexBar/MenuCardView.swift | 13 +- .../SettingsStore+MenuObservation.swift | 1 + .../MenuCardModelCodexProjectionTests.swift | 192 ++++++++++++++++++ Tests/CodexBarTests/SettingsStoreTests.swift | 27 +++ 5 files changed, 275 insertions(+), 8 deletions(-) diff --git a/Sources/CodexBar/MenuCardQuotaWarningMarkers.swift b/Sources/CodexBar/MenuCardQuotaWarningMarkers.swift index c5f0c1852..abe15e287 100644 --- a/Sources/CodexBar/MenuCardQuotaWarningMarkers.swift +++ b/Sources/CodexBar/MenuCardQuotaWarningMarkers.swift @@ -18,6 +18,56 @@ extension UsageMenuCardView.Model { .map { showUsed ? 100 - Double($0) : Double($0) } .filter { $0 > 0 && $0 < 100 } } + + /// Merges quota warning markers with optional work-day boundary markers. + /// Preserves original warning-marker ordering when workdayMarkers is empty, + /// sorts the combined set when workday markers are present. + static func mergedMarkerPercents( + warningMarkers: [Double], + workdayMarkers: [Double]) -> [Double] + { + let combined = warningMarkers + workdayMarkers + return workdayMarkers.isEmpty ? combined : combined.sorted() + } + + /// Combines quota warning markers with optional work-day boundary markers + /// into a single sorted array. Workday markers are only applied when + /// includeWorkdayMarkers is true and windowMinutes == 10080. + static func markerPercents( + thresholds: [Int]?, + showUsed: Bool, + workDays: Int?, + windowMinutes: Int?, + includeWorkdayMarkers: Bool) -> [Double] + { + let warningMarkers = Self.warningMarkerPercents(thresholds: thresholds, showUsed: showUsed) + let workdayMarkers = includeWorkdayMarkers + ? workDayMarkerPercents(workDays: workDays, windowMinutes: windowMinutes) + : [] + return Self.mergedMarkerPercents(warningMarkers: warningMarkers, workdayMarkers: workdayMarkers) + } + + static func weeklyMarkerPercents(input: Input, windowMinutes: Int?) -> [Double] { + UsageMenuCardView.Model.markerPercents( + thresholds: input.quotaWarningThresholds[.weekly], + showUsed: input.usageBarsShowUsed, + workDays: input.workDaysPerWeek, + windowMinutes: windowMinutes, + includeWorkdayMarkers: true) + } + + static func codexLaneMarkerPercents( + input: Input, + lane: CodexConsumerProjection.RateLane, + windowMinutes: Int?) -> [Double] + { + UsageMenuCardView.Model.markerPercents( + thresholds: input.quotaWarningThresholds[lane.quotaWarningWindow], + showUsed: input.usageBarsShowUsed, + workDays: input.workDaysPerWeek, + windowMinutes: windowMinutes, + includeWorkdayMarkers: lane == .weekly) + } } /// Returns boundary percentages for work day markers on a weekly progress bar. diff --git a/Sources/CodexBar/MenuCardView.swift b/Sources/CodexBar/MenuCardView.swift index 52f57b2df..ed423a460 100644 --- a/Sources/CodexBar/MenuCardView.swift +++ b/Sources/CodexBar/MenuCardView.swift @@ -1386,11 +1386,7 @@ extension UsageMenuCardView.Model { detailRightText: paceDetail?.rightLabel, pacePercent: paceDetail?.pacePercent, paceOnTop: paceDetail?.paceOnTop ?? true, - warningMarkerPercents: (Self.warningMarkerPercents( - thresholds: input.quotaWarningThresholds[.weekly], - showUsed: input.usageBarsShowUsed) + workDayMarkerPercents( - workDays: input.workDaysPerWeek, - windowMinutes: weekly.windowMinutes)).sorted()) + warningMarkerPercents: Self.weeklyMarkerPercents(input: input, windowMinutes: weekly.windowMinutes)) } private static func codexRateMetrics( @@ -1436,9 +1432,10 @@ extension UsageMenuCardView.Model { detailRightText: paceDetail?.rightLabel, pacePercent: paceDetail?.pacePercent, paceOnTop: paceDetail?.paceOnTop ?? true, - warningMarkerPercents: Self.warningMarkerPercents( - thresholds: input.quotaWarningThresholds[lane.quotaWarningWindow], - showUsed: input.usageBarsShowUsed)) + warningMarkerPercents: Self.codexLaneMarkerPercents( + input: input, + lane: lane, + windowMinutes: window.windowMinutes)) } } diff --git a/Sources/CodexBar/SettingsStore+MenuObservation.swift b/Sources/CodexBar/SettingsStore+MenuObservation.swift index 2af6b263b..5de81817e 100644 --- a/Sources/CodexBar/SettingsStore+MenuObservation.swift +++ b/Sources/CodexBar/SettingsStore+MenuObservation.swift @@ -19,6 +19,7 @@ extension SettingsStore { _ = self.quotaWarningWindowEnabled(.weekly) _ = self.quotaWarningSoundEnabled _ = self.quotaWarningMarkersVisible + _ = self.weeklyProgressWorkDays _ = self.usageBarsShowUsed _ = self.resetTimesShowAbsolute _ = self.providerChangelogLinksEnabled diff --git a/Tests/CodexBarTests/MenuCardModelCodexProjectionTests.swift b/Tests/CodexBarTests/MenuCardModelCodexProjectionTests.swift index 3f9a2f6c0..f96d441f1 100644 --- a/Tests/CodexBarTests/MenuCardModelCodexProjectionTests.swift +++ b/Tests/CodexBarTests/MenuCardModelCodexProjectionTests.swift @@ -67,6 +67,198 @@ struct MenuCardModelCodexProjectionTests { #expect(weekly.detailRightText == "Lasts until reset") } + @Test + func `codex weekly lane includes workday markers when workDaysPerWeek is set`() throws { + let now = Date(timeIntervalSince1970: 1_800_000_000) + let metadata = try #require(ProviderDefaults.metadata[.codex]) + let identity = ProviderIdentitySnapshot( + providerID: .codex, + accountEmail: "user@example.com", + accountOrganization: nil, + loginMethod: "Pro") + let snapshot = UsageSnapshot( + primary: RateWindow( + usedPercent: 2, + windowMinutes: 300, + resetsAt: now.addingTimeInterval(4 * 60 * 60), + resetDescription: nil), + secondary: RateWindow( + usedPercent: 4, + windowMinutes: 10080, + resetsAt: now.addingTimeInterval(6 * 24 * 60 * 60), + resetDescription: nil), + tertiary: nil, + updatedAt: now, + identity: identity) + let projection = CodexConsumerProjection.make( + surface: .liveCard, + context: CodexConsumerProjection.Context( + snapshot: snapshot, + rawUsageError: nil, + liveCredits: nil, + rawCreditsError: nil, + liveDashboard: nil, + rawDashboardError: nil, + dashboardAttachmentAuthorized: false, + dashboardRequiresLogin: false, + now: now)) + + let model = UsageMenuCardView.Model.make(.init( + provider: .codex, + metadata: metadata, + snapshot: snapshot, + codexProjection: projection, + credits: nil, + creditsError: nil, + dashboard: nil, + dashboardError: nil, + tokenSnapshot: nil, + tokenError: nil, + account: AccountInfo(email: "user@example.com", plan: "Pro"), + isRefreshing: false, + lastError: nil, + usageBarsShowUsed: false, + resetTimeDisplayStyle: .countdown, + tokenCostUsageEnabled: false, + showOptionalCreditsAndExtraUsage: true, + hidePersonalInfo: false, + quotaWarningThresholds: [.session: [], .weekly: []], + workDaysPerWeek: 5, + now: now)) + + let weekly = try #require(model.metrics.first { $0.id == "secondary" }) + #expect(weekly.warningMarkerPercents == [20.0, 40.0, 60.0, 80.0]) + + let session = try #require(model.metrics.first { $0.id == "primary" }) + #expect(session.warningMarkerPercents.isEmpty) + } + + @Test + func `codex weekly lane workday markers merge with quota warning markers`() throws { + let now = Date(timeIntervalSince1970: 1_800_000_000) + let metadata = try #require(ProviderDefaults.metadata[.codex]) + let identity = ProviderIdentitySnapshot( + providerID: .codex, + accountEmail: "user@example.com", + accountOrganization: nil, + loginMethod: "Pro") + let snapshot = UsageSnapshot( + primary: RateWindow( + usedPercent: 2, + windowMinutes: 300, + resetsAt: now.addingTimeInterval(4 * 60 * 60), + resetDescription: nil), + secondary: RateWindow( + usedPercent: 4, + windowMinutes: 10080, + resetsAt: now.addingTimeInterval(6 * 24 * 60 * 60), + resetDescription: nil), + tertiary: nil, + updatedAt: now, + identity: identity) + let projection = CodexConsumerProjection.make( + surface: .liveCard, + context: CodexConsumerProjection.Context( + snapshot: snapshot, + rawUsageError: nil, + liveCredits: nil, + rawCreditsError: nil, + liveDashboard: nil, + rawDashboardError: nil, + dashboardAttachmentAuthorized: false, + dashboardRequiresLogin: false, + now: now)) + + let model = UsageMenuCardView.Model.make(.init( + provider: .codex, + metadata: metadata, + snapshot: snapshot, + codexProjection: projection, + credits: nil, + creditsError: nil, + dashboard: nil, + dashboardError: nil, + tokenSnapshot: nil, + tokenError: nil, + account: AccountInfo(email: "user@example.com", plan: "Pro"), + isRefreshing: false, + lastError: nil, + usageBarsShowUsed: false, + resetTimeDisplayStyle: .countdown, + tokenCostUsageEnabled: false, + showOptionalCreditsAndExtraUsage: true, + hidePersonalInfo: false, + quotaWarningThresholds: [.session: [], .weekly: [50]], + workDaysPerWeek: 5, + now: now)) + + let weekly = try #require(model.metrics.first { $0.id == "secondary" }) + #expect(weekly.warningMarkerPercents == [20.0, 40.0, 50.0, 60.0, 80.0]) + } + + @Test + func `codex weekly lane workday markers not inverted by usageBarsShowUsed`() throws { + let now = Date(timeIntervalSince1970: 1_800_000_000) + let metadata = try #require(ProviderDefaults.metadata[.codex]) + let identity = ProviderIdentitySnapshot( + providerID: .codex, + accountEmail: "user@example.com", + accountOrganization: nil, + loginMethod: "Pro") + let snapshot = UsageSnapshot( + primary: RateWindow( + usedPercent: 2, + windowMinutes: 300, + resetsAt: now.addingTimeInterval(4 * 60 * 60), + resetDescription: nil), + secondary: RateWindow( + usedPercent: 4, + windowMinutes: 10080, + resetsAt: now.addingTimeInterval(6 * 24 * 60 * 60), + resetDescription: nil), + tertiary: nil, + updatedAt: now, + identity: identity) + let projection = CodexConsumerProjection.make( + surface: .liveCard, + context: CodexConsumerProjection.Context( + snapshot: snapshot, + rawUsageError: nil, + liveCredits: nil, + rawCreditsError: nil, + liveDashboard: nil, + rawDashboardError: nil, + dashboardAttachmentAuthorized: false, + dashboardRequiresLogin: false, + now: now)) + + let model = UsageMenuCardView.Model.make(.init( + provider: .codex, + metadata: metadata, + snapshot: snapshot, + codexProjection: projection, + credits: nil, + creditsError: nil, + dashboard: nil, + dashboardError: nil, + tokenSnapshot: nil, + tokenError: nil, + account: AccountInfo(email: "user@example.com", plan: "Pro"), + isRefreshing: false, + lastError: nil, + usageBarsShowUsed: true, + resetTimeDisplayStyle: .countdown, + tokenCostUsageEnabled: false, + showOptionalCreditsAndExtraUsage: true, + hidePersonalInfo: false, + quotaWarningThresholds: [.session: [], .weekly: []], + workDaysPerWeek: 5, + now: now)) + + let weekly = try #require(model.metrics.first { $0.id == "secondary" }) + #expect(weekly.warningMarkerPercents == [20.0, 40.0, 60.0, 80.0]) + } + @Test func `codex plan only snapshot shows limits unavailable placeholder`() throws { let now = Date() diff --git a/Tests/CodexBarTests/SettingsStoreTests.swift b/Tests/CodexBarTests/SettingsStoreTests.swift index 4182f310d..7c3b70790 100644 --- a/Tests/CodexBarTests/SettingsStoreTests.swift +++ b/Tests/CodexBarTests/SettingsStoreTests.swift @@ -1068,6 +1068,33 @@ struct SettingsStoreTests { await expectObservation(for: .weekly, thresholds: [80, 40]) } + @Test + func `menu observation token updates on weekly progress work days changes`() async throws { + let suite = "SettingsStoreTests-observation-weekly-progress-work-days" + let defaults = try #require(UserDefaults(suiteName: suite)) + defaults.removePersistentDomain(forName: suite) + let configStore = testConfigStore(suiteName: suite) + + let store = SettingsStore( + userDefaults: defaults, + configStore: configStore, + zaiTokenStore: NoopZaiTokenStore(), + syntheticTokenStore: NoopSyntheticTokenStore()) + + let didChange = ObservationFlag() + + withObservationTracking { + _ = store.menuObservationToken + } onChange: { + didChange.set() + } + + store.weeklyProgressWorkDays = 5 + try? await Task.sleep(nanoseconds: 50_000_000) + + #expect(didChange.get() == true) + } + @Test func `config backed settings trigger observation`() async throws { let suite = "SettingsStoreTests-observation-config" From ebf2e2f01318453fc83e55537a810287e5c2f7a3 Mon Sep 17 00:00:00 2001 From: Yuxin Qiao <104957188+Yuxin-Qiao@users.noreply.github.com> Date: Sat, 23 May 2026 01:21:14 +0800 Subject: [PATCH 3/4] Linux: import FoundationNetworking for HTTPCookie --- .../Providers/Alibaba/AlibabaTokenPlanCookieHeader.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Sources/CodexBarCore/Providers/Alibaba/AlibabaTokenPlanCookieHeader.swift b/Sources/CodexBarCore/Providers/Alibaba/AlibabaTokenPlanCookieHeader.swift index 03791cc95..60c2ab24b 100644 --- a/Sources/CodexBarCore/Providers/Alibaba/AlibabaTokenPlanCookieHeader.swift +++ b/Sources/CodexBarCore/Providers/Alibaba/AlibabaTokenPlanCookieHeader.swift @@ -1,4 +1,7 @@ import Foundation +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif struct AlibabaTokenPlanCookieHeaders: Sendable { private static let cachedAPIHeaderName = "__codexbar_alibaba_token_plan_api" From 6e94bc3c97b1aed8f21296f76280a0e80d800f12 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 22 May 2026 23:36:00 +0100 Subject: [PATCH 4/4] docs: update changelog for workday markers --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d96ee94aa..1b65a4ea8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## 0.29.1 — Unreleased ### Added +- Display: add optional workday markers for weekly progress bars (#1102). Thanks @Yuxin-Qiao! ### Fixed - Menu bar: show extra-usage spend as currency text for Claude and Cursor when that metric is selected (#1107). Thanks @Yuxin-Qiao!