Skip to content

Commit d8aa688

Browse files
committed
Fix weekly workday marker observation and codex lanes
1 parent 472f90d commit d8aa688

5 files changed

Lines changed: 275 additions & 8 deletions

File tree

Sources/CodexBar/MenuCardQuotaWarningMarkers.swift

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,56 @@ extension UsageMenuCardView.Model {
1818
.map { showUsed ? 100 - Double($0) : Double($0) }
1919
.filter { $0 > 0 && $0 < 100 }
2020
}
21+
22+
/// Merges quota warning markers with optional work-day boundary markers.
23+
/// Preserves original warning-marker ordering when workdayMarkers is empty,
24+
/// sorts the combined set when workday markers are present.
25+
static func mergedMarkerPercents(
26+
warningMarkers: [Double],
27+
workdayMarkers: [Double]) -> [Double]
28+
{
29+
let combined = warningMarkers + workdayMarkers
30+
return workdayMarkers.isEmpty ? combined : combined.sorted()
31+
}
32+
33+
/// Combines quota warning markers with optional work-day boundary markers
34+
/// into a single sorted array. Workday markers are only applied when
35+
/// includeWorkdayMarkers is true and windowMinutes == 10080.
36+
static func markerPercents(
37+
thresholds: [Int]?,
38+
showUsed: Bool,
39+
workDays: Int?,
40+
windowMinutes: Int?,
41+
includeWorkdayMarkers: Bool) -> [Double]
42+
{
43+
let warningMarkers = Self.warningMarkerPercents(thresholds: thresholds, showUsed: showUsed)
44+
let workdayMarkers = includeWorkdayMarkers
45+
? workDayMarkerPercents(workDays: workDays, windowMinutes: windowMinutes)
46+
: []
47+
return Self.mergedMarkerPercents(warningMarkers: warningMarkers, workdayMarkers: workdayMarkers)
48+
}
49+
50+
static func weeklyMarkerPercents(input: Input, windowMinutes: Int?) -> [Double] {
51+
UsageMenuCardView.Model.markerPercents(
52+
thresholds: input.quotaWarningThresholds[.weekly],
53+
showUsed: input.usageBarsShowUsed,
54+
workDays: input.workDaysPerWeek,
55+
windowMinutes: windowMinutes,
56+
includeWorkdayMarkers: true)
57+
}
58+
59+
static func codexLaneMarkerPercents(
60+
input: Input,
61+
lane: CodexConsumerProjection.RateLane,
62+
windowMinutes: Int?) -> [Double]
63+
{
64+
UsageMenuCardView.Model.markerPercents(
65+
thresholds: input.quotaWarningThresholds[lane.quotaWarningWindow],
66+
showUsed: input.usageBarsShowUsed,
67+
workDays: input.workDaysPerWeek,
68+
windowMinutes: windowMinutes,
69+
includeWorkdayMarkers: lane == .weekly)
70+
}
2171
}
2272

2373
/// Returns boundary percentages for work day markers on a weekly progress bar.

Sources/CodexBar/MenuCardView.swift

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1386,11 +1386,7 @@ extension UsageMenuCardView.Model {
13861386
detailRightText: paceDetail?.rightLabel,
13871387
pacePercent: paceDetail?.pacePercent,
13881388
paceOnTop: paceDetail?.paceOnTop ?? true,
1389-
warningMarkerPercents: (Self.warningMarkerPercents(
1390-
thresholds: input.quotaWarningThresholds[.weekly],
1391-
showUsed: input.usageBarsShowUsed) + workDayMarkerPercents(
1392-
workDays: input.workDaysPerWeek,
1393-
windowMinutes: weekly.windowMinutes)).sorted())
1389+
warningMarkerPercents: Self.weeklyMarkerPercents(input: input, windowMinutes: weekly.windowMinutes))
13941390
}
13951391

13961392
private static func codexRateMetrics(
@@ -1436,9 +1432,10 @@ extension UsageMenuCardView.Model {
14361432
detailRightText: paceDetail?.rightLabel,
14371433
pacePercent: paceDetail?.pacePercent,
14381434
paceOnTop: paceDetail?.paceOnTop ?? true,
1439-
warningMarkerPercents: Self.warningMarkerPercents(
1440-
thresholds: input.quotaWarningThresholds[lane.quotaWarningWindow],
1441-
showUsed: input.usageBarsShowUsed))
1435+
warningMarkerPercents: Self.codexLaneMarkerPercents(
1436+
input: input,
1437+
lane: lane,
1438+
windowMinutes: window.windowMinutes))
14421439
}
14431440
}
14441441

Sources/CodexBar/SettingsStore+MenuObservation.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ extension SettingsStore {
1919
_ = self.quotaWarningWindowEnabled(.weekly)
2020
_ = self.quotaWarningSoundEnabled
2121
_ = self.quotaWarningMarkersVisible
22+
_ = self.weeklyProgressWorkDays
2223
_ = self.usageBarsShowUsed
2324
_ = self.resetTimesShowAbsolute
2425
_ = self.providerChangelogLinksEnabled

Tests/CodexBarTests/MenuCardModelCodexProjectionTests.swift

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,198 @@ struct MenuCardModelCodexProjectionTests {
6767
#expect(weekly.detailRightText == "Lasts until reset")
6868
}
6969

70+
@Test
71+
func `codex weekly lane includes workday markers when workDaysPerWeek is set`() throws {
72+
let now = Date(timeIntervalSince1970: 1_800_000_000)
73+
let metadata = try #require(ProviderDefaults.metadata[.codex])
74+
let identity = ProviderIdentitySnapshot(
75+
providerID: .codex,
76+
accountEmail: "[email protected]",
77+
accountOrganization: nil,
78+
loginMethod: "Pro")
79+
let snapshot = UsageSnapshot(
80+
primary: RateWindow(
81+
usedPercent: 2,
82+
windowMinutes: 300,
83+
resetsAt: now.addingTimeInterval(4 * 60 * 60),
84+
resetDescription: nil),
85+
secondary: RateWindow(
86+
usedPercent: 4,
87+
windowMinutes: 10080,
88+
resetsAt: now.addingTimeInterval(6 * 24 * 60 * 60),
89+
resetDescription: nil),
90+
tertiary: nil,
91+
updatedAt: now,
92+
identity: identity)
93+
let projection = CodexConsumerProjection.make(
94+
surface: .liveCard,
95+
context: CodexConsumerProjection.Context(
96+
snapshot: snapshot,
97+
rawUsageError: nil,
98+
liveCredits: nil,
99+
rawCreditsError: nil,
100+
liveDashboard: nil,
101+
rawDashboardError: nil,
102+
dashboardAttachmentAuthorized: false,
103+
dashboardRequiresLogin: false,
104+
now: now))
105+
106+
let model = UsageMenuCardView.Model.make(.init(
107+
provider: .codex,
108+
metadata: metadata,
109+
snapshot: snapshot,
110+
codexProjection: projection,
111+
credits: nil,
112+
creditsError: nil,
113+
dashboard: nil,
114+
dashboardError: nil,
115+
tokenSnapshot: nil,
116+
tokenError: nil,
117+
account: AccountInfo(email: "[email protected]", plan: "Pro"),
118+
isRefreshing: false,
119+
lastError: nil,
120+
usageBarsShowUsed: false,
121+
resetTimeDisplayStyle: .countdown,
122+
tokenCostUsageEnabled: false,
123+
showOptionalCreditsAndExtraUsage: true,
124+
hidePersonalInfo: false,
125+
quotaWarningThresholds: [.session: [], .weekly: []],
126+
workDaysPerWeek: 5,
127+
now: now))
128+
129+
let weekly = try #require(model.metrics.first { $0.id == "secondary" })
130+
#expect(weekly.warningMarkerPercents == [20.0, 40.0, 60.0, 80.0])
131+
132+
let session = try #require(model.metrics.first { $0.id == "primary" })
133+
#expect(session.warningMarkerPercents.isEmpty)
134+
}
135+
136+
@Test
137+
func `codex weekly lane workday markers merge with quota warning markers`() throws {
138+
let now = Date(timeIntervalSince1970: 1_800_000_000)
139+
let metadata = try #require(ProviderDefaults.metadata[.codex])
140+
let identity = ProviderIdentitySnapshot(
141+
providerID: .codex,
142+
accountEmail: "[email protected]",
143+
accountOrganization: nil,
144+
loginMethod: "Pro")
145+
let snapshot = UsageSnapshot(
146+
primary: RateWindow(
147+
usedPercent: 2,
148+
windowMinutes: 300,
149+
resetsAt: now.addingTimeInterval(4 * 60 * 60),
150+
resetDescription: nil),
151+
secondary: RateWindow(
152+
usedPercent: 4,
153+
windowMinutes: 10080,
154+
resetsAt: now.addingTimeInterval(6 * 24 * 60 * 60),
155+
resetDescription: nil),
156+
tertiary: nil,
157+
updatedAt: now,
158+
identity: identity)
159+
let projection = CodexConsumerProjection.make(
160+
surface: .liveCard,
161+
context: CodexConsumerProjection.Context(
162+
snapshot: snapshot,
163+
rawUsageError: nil,
164+
liveCredits: nil,
165+
rawCreditsError: nil,
166+
liveDashboard: nil,
167+
rawDashboardError: nil,
168+
dashboardAttachmentAuthorized: false,
169+
dashboardRequiresLogin: false,
170+
now: now))
171+
172+
let model = UsageMenuCardView.Model.make(.init(
173+
provider: .codex,
174+
metadata: metadata,
175+
snapshot: snapshot,
176+
codexProjection: projection,
177+
credits: nil,
178+
creditsError: nil,
179+
dashboard: nil,
180+
dashboardError: nil,
181+
tokenSnapshot: nil,
182+
tokenError: nil,
183+
account: AccountInfo(email: "[email protected]", plan: "Pro"),
184+
isRefreshing: false,
185+
lastError: nil,
186+
usageBarsShowUsed: false,
187+
resetTimeDisplayStyle: .countdown,
188+
tokenCostUsageEnabled: false,
189+
showOptionalCreditsAndExtraUsage: true,
190+
hidePersonalInfo: false,
191+
quotaWarningThresholds: [.session: [], .weekly: [50]],
192+
workDaysPerWeek: 5,
193+
now: now))
194+
195+
let weekly = try #require(model.metrics.first { $0.id == "secondary" })
196+
#expect(weekly.warningMarkerPercents == [20.0, 40.0, 50.0, 60.0, 80.0])
197+
}
198+
199+
@Test
200+
func `codex weekly lane workday markers not inverted by usageBarsShowUsed`() throws {
201+
let now = Date(timeIntervalSince1970: 1_800_000_000)
202+
let metadata = try #require(ProviderDefaults.metadata[.codex])
203+
let identity = ProviderIdentitySnapshot(
204+
providerID: .codex,
205+
accountEmail: "[email protected]",
206+
accountOrganization: nil,
207+
loginMethod: "Pro")
208+
let snapshot = UsageSnapshot(
209+
primary: RateWindow(
210+
usedPercent: 2,
211+
windowMinutes: 300,
212+
resetsAt: now.addingTimeInterval(4 * 60 * 60),
213+
resetDescription: nil),
214+
secondary: RateWindow(
215+
usedPercent: 4,
216+
windowMinutes: 10080,
217+
resetsAt: now.addingTimeInterval(6 * 24 * 60 * 60),
218+
resetDescription: nil),
219+
tertiary: nil,
220+
updatedAt: now,
221+
identity: identity)
222+
let projection = CodexConsumerProjection.make(
223+
surface: .liveCard,
224+
context: CodexConsumerProjection.Context(
225+
snapshot: snapshot,
226+
rawUsageError: nil,
227+
liveCredits: nil,
228+
rawCreditsError: nil,
229+
liveDashboard: nil,
230+
rawDashboardError: nil,
231+
dashboardAttachmentAuthorized: false,
232+
dashboardRequiresLogin: false,
233+
now: now))
234+
235+
let model = UsageMenuCardView.Model.make(.init(
236+
provider: .codex,
237+
metadata: metadata,
238+
snapshot: snapshot,
239+
codexProjection: projection,
240+
credits: nil,
241+
creditsError: nil,
242+
dashboard: nil,
243+
dashboardError: nil,
244+
tokenSnapshot: nil,
245+
tokenError: nil,
246+
account: AccountInfo(email: "[email protected]", plan: "Pro"),
247+
isRefreshing: false,
248+
lastError: nil,
249+
usageBarsShowUsed: true,
250+
resetTimeDisplayStyle: .countdown,
251+
tokenCostUsageEnabled: false,
252+
showOptionalCreditsAndExtraUsage: true,
253+
hidePersonalInfo: false,
254+
quotaWarningThresholds: [.session: [], .weekly: []],
255+
workDaysPerWeek: 5,
256+
now: now))
257+
258+
let weekly = try #require(model.metrics.first { $0.id == "secondary" })
259+
#expect(weekly.warningMarkerPercents == [20.0, 40.0, 60.0, 80.0])
260+
}
261+
70262
@Test
71263
func `codex plan only snapshot shows limits unavailable placeholder`() throws {
72264
let now = Date()

Tests/CodexBarTests/SettingsStoreTests.swift

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1068,6 +1068,33 @@ struct SettingsStoreTests {
10681068
await expectObservation(for: .weekly, thresholds: [80, 40])
10691069
}
10701070

1071+
@Test
1072+
func `menu observation token updates on weekly progress work days changes`() async throws {
1073+
let suite = "SettingsStoreTests-observation-weekly-progress-work-days"
1074+
let defaults = try #require(UserDefaults(suiteName: suite))
1075+
defaults.removePersistentDomain(forName: suite)
1076+
let configStore = testConfigStore(suiteName: suite)
1077+
1078+
let store = SettingsStore(
1079+
userDefaults: defaults,
1080+
configStore: configStore,
1081+
zaiTokenStore: NoopZaiTokenStore(),
1082+
syntheticTokenStore: NoopSyntheticTokenStore())
1083+
1084+
let didChange = ObservationFlag()
1085+
1086+
withObservationTracking {
1087+
_ = store.menuObservationToken
1088+
} onChange: {
1089+
didChange.set()
1090+
}
1091+
1092+
store.weeklyProgressWorkDays = 5
1093+
try? await Task.sleep(nanoseconds: 50_000_000)
1094+
1095+
#expect(didChange.get() == true)
1096+
}
1097+
10711098
@Test
10721099
func `config backed settings trigger observation`() async throws {
10731100
let suite = "SettingsStoreTests-observation-config"

0 commit comments

Comments
 (0)