Skip to content

Commit 66df58d

Browse files
AlfredAlfred
authored andcommitted
Address review: filter persisted scoped rows on fallback, default setting off
1 parent ee2f8ce commit 66df58d

10 files changed

Lines changed: 224 additions & 83 deletions

Sources/CodexBar/SettingsStore.swift

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -507,12 +507,9 @@ extension SettingsStore {
507507
if Self.isRunningTests, claudeDailyRoutinesUsageVisibleDefault == nil {
508508
userDefaults.set(true, forKey: "claudeDailyRoutinesUsageVisible")
509509
}
510-
let claudeModelScopedWeeklyUsageVisibleDefault = userDefaults.object(
511-
forKey: "claudeModelScopedWeeklyUsageVisible") as? Bool
512-
let claudeModelScopedWeeklyUsageVisible = claudeModelScopedWeeklyUsageVisibleDefault ?? true
513-
if Self.isRunningTests, claudeModelScopedWeeklyUsageVisibleDefault == nil {
514-
userDefaults.set(true, forKey: "claudeModelScopedWeeklyUsageVisible")
515-
}
510+
// Model-scoped weekly rows are opt-in: a fresh install keeps widgets on the standard quota lanes.
511+
let claudeModelScopedWeeklyUsageVisible = userDefaults.object(
512+
forKey: "claudeModelScopedWeeklyUsageVisible") as? Bool ?? false
516513
let codexSparkUsageVisibleDefault = userDefaults.object(forKey: "codexSparkUsageVisible") as? Bool
517514
let codexSparkUsageVisible = codexSparkUsageVisibleDefault ?? true
518515
if Self.isRunningTests, codexSparkUsageVisibleDefault == nil {

Sources/CodexBar/UsageStore+WidgetSnapshot.swift

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,8 @@ extension UsageStore {
206206
{
207207
Self.preservedClaudeWidgetUsage(
208208
from: previousEntry,
209-
expectedQuotaOwnerKey: claudeQuotaOwnerKey)
209+
expectedQuotaOwnerKey: claudeQuotaOwnerKey,
210+
includesModelScopedWeeklyRows: self.settings.claudeModelScopedWeeklyUsageVisible)
210211
} else {
211212
nil
212213
}
@@ -294,7 +295,8 @@ extension UsageStore {
294295

295296
private nonisolated static func preservedClaudeWidgetUsage(
296297
from entry: WidgetSnapshot.ProviderEntry?,
297-
expectedQuotaOwnerKey: String?) -> PreservedClaudeWidgetUsage?
298+
expectedQuotaOwnerKey: String?,
299+
includesModelScopedWeeklyRows: Bool) -> PreservedClaudeWidgetUsage?
298300
{
299301
guard let entry, entry.provider == .claude else { return nil }
300302
guard let expectedQuotaOwnerKey,
@@ -309,6 +311,13 @@ extension UsageStore {
309311
let tertiary = entry.tertiary?.isSyntheticPlaceholder == true ? nil : entry.tertiary
310312
let usageRows = entry.usageRows?.filter { row in
311313
guard row.window?.isSyntheticPlaceholder != true else { return false }
314+
// Rows persisted while the setting was on must not outlive it: without a live snapshot
315+
// this preserved list is what widgets render, so re-apply the visibility filter here.
316+
guard includesModelScopedWeeklyRows ||
317+
!row.id.hasPrefix(Self.claudeModelScopedWeeklyWindowIDPrefix)
318+
else {
319+
return false
320+
}
312321
return switch row.id {
313322
case "primary": primary != nil
314323
case "secondary": secondary != nil
@@ -471,7 +480,9 @@ extension UsageStore {
471480
// Claude fetchers place model-scoped weekly quotas (for example, Fable) in extraRateWindows.
472481
// Keep the widget projection generic so newly surfaced Claude model quotas appear without UI changes.
473482
rows.append(contentsOf: (snapshot.extraRateWindows ?? []).compactMap { namedWindow in
474-
guard namedWindow.id.hasPrefix("claude-weekly-scoped-"), namedWindow.usageKnown else { return nil }
483+
guard namedWindow.id.hasPrefix(Self.claudeModelScopedWeeklyWindowIDPrefix),
484+
namedWindow.usageKnown
485+
else { return nil }
475486
return WidgetSnapshot.WidgetUsageRowSnapshot(
476487
id: namedWindow.id,
477488
title: namedWindow.title,
@@ -494,6 +505,9 @@ extension UsageStore {
494505
return rows.filter { $0.percentLeft != nil }
495506
}
496507

508+
/// Identifier prefix Claude fetchers use for model-scoped weekly carve-outs (for example, Fable).
509+
private nonisolated static let claudeModelScopedWeeklyWindowIDPrefix = "claude-weekly-scoped-"
510+
497511
private nonisolated static let antigravityQuotaSummaryWindowIDPrefix = "antigravity-quota-summary-"
498512
private nonisolated static let antigravityCompactFallbackWindowIDPrefix = "antigravity-compact-fallback-"
499513

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import CodexBarCore
2+
import Foundation
3+
import Observation
4+
import Testing
5+
@testable import CodexBar
6+
7+
@Suite(.serialized)
8+
@MainActor
9+
struct ClaudeScopedWeeklySettingsTests {
10+
private final class ObservationFlag: @unchecked Sendable {
11+
private let lock = NSLock()
12+
private var value = false
13+
14+
func set() {
15+
self.lock.lock()
16+
self.value = true
17+
self.lock.unlock()
18+
}
19+
20+
func get() -> Bool {
21+
self.lock.lock()
22+
defer { self.lock.unlock() }
23+
return self.value
24+
}
25+
}
26+
27+
@Test
28+
func `Claude model scoped widget usage defaults off persists and refreshes only menus`() async throws {
29+
let suite = "ClaudeScopedWeeklySettingsTests-claude-model-scoped-widget-usage-visible"
30+
let defaults = try #require(UserDefaults(suiteName: suite))
31+
defaults.removePersistentDomain(forName: suite)
32+
let configStore = testConfigStore(suiteName: suite)
33+
let store = SettingsStore(
34+
userDefaults: defaults,
35+
configStore: configStore,
36+
zaiTokenStore: NoopZaiTokenStore(),
37+
syntheticTokenStore: NoopSyntheticTokenStore())
38+
39+
#expect(store.claudeModelScopedWeeklyUsageVisible == false)
40+
let backgroundRevision = store.backgroundWorkSettingsRevision
41+
let menuDidChange = ObservationFlag()
42+
withObservationTracking {
43+
_ = store.menuObservationToken
44+
} onChange: {
45+
menuDidChange.set()
46+
}
47+
store.claudeModelScopedWeeklyUsageVisible = true
48+
try? await Task.sleep(nanoseconds: 50_000_000)
49+
#expect(store.backgroundWorkSettingsRevision == backgroundRevision)
50+
#expect(menuDidChange.get())
51+
52+
let reloaded = SettingsStore(
53+
userDefaults: defaults,
54+
configStore: configStore,
55+
zaiTokenStore: NoopZaiTokenStore(),
56+
syntheticTokenStore: NoopSyntheticTokenStore())
57+
#expect(reloaded.claudeModelScopedWeeklyUsageVisible)
58+
}
59+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import Foundation
2+
import Testing
3+
@testable import CodexBarCore
4+
@testable import CodexBarWidget
5+
6+
struct ClaudeScopedWeeklyWidgetRowTests {
7+
@Test
8+
func `Claude widget renders projected model scoped weekly rows`() {
9+
let entry = WidgetSnapshot.ProviderEntry(
10+
provider: .claude,
11+
updatedAt: Date(),
12+
primary: nil,
13+
secondary: nil,
14+
tertiary: nil,
15+
usageRows: [
16+
WidgetSnapshot.WidgetUsageRowSnapshot(id: "primary", title: "Session", percentLeft: 75),
17+
WidgetSnapshot.WidgetUsageRowSnapshot(id: "secondary", title: "Weekly", percentLeft: 50),
18+
WidgetSnapshot.WidgetUsageRowSnapshot(
19+
id: "claude-weekly-scoped-fable",
20+
title: "Fable only",
21+
percentLeft: 70),
22+
],
23+
creditsRemaining: nil,
24+
codeReviewRemainingPercent: nil,
25+
tokenUsage: nil,
26+
dailyUsage: [])
27+
28+
#expect(WidgetUsageRow.smallWidgetRowLimit(for: entry) == nil)
29+
#expect(WidgetUsageRow.rows(for: entry).map(\.id) == [
30+
"primary",
31+
"secondary",
32+
"claude-weekly-scoped-fable",
33+
])
34+
}
35+
}

Tests/CodexBarTests/CodexBarWidgetProviderTests.swift

Lines changed: 0 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -567,35 +567,6 @@ struct CodexBarWidgetProviderTests {
567567
#expect(rows == [WidgetUsageRow(id: "weekly", title: "Weekly", percentLeft: 75)])
568568
}
569569

570-
@Test
571-
func `Claude widget renders projected model scoped weekly rows`() {
572-
let entry = WidgetSnapshot.ProviderEntry(
573-
provider: .claude,
574-
updatedAt: Date(),
575-
primary: nil,
576-
secondary: nil,
577-
tertiary: nil,
578-
usageRows: [
579-
WidgetSnapshot.WidgetUsageRowSnapshot(id: "primary", title: "Session", percentLeft: 75),
580-
WidgetSnapshot.WidgetUsageRowSnapshot(id: "secondary", title: "Weekly", percentLeft: 50),
581-
WidgetSnapshot.WidgetUsageRowSnapshot(
582-
id: "claude-weekly-scoped-fable",
583-
title: "Fable only",
584-
percentLeft: 70),
585-
],
586-
creditsRemaining: nil,
587-
codeReviewRemainingPercent: nil,
588-
tokenUsage: nil,
589-
dailyUsage: [])
590-
591-
#expect(WidgetUsageRow.smallWidgetRowLimit(for: entry) == nil)
592-
#expect(WidgetUsageRow.rows(for: entry).map(\.id) == [
593-
"primary",
594-
"secondary",
595-
"claude-weekly-scoped-fable",
596-
])
597-
}
598-
599570
@Test
600571
func `codex widget session cap lifts at weekly reset without a new snapshot`() {
601572
let now = Date(timeIntervalSince1970: 1_800_000_000)

Tests/CodexBarTests/ProviderSettingsDescriptorTests.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -345,20 +345,20 @@ struct ProviderSettingsDescriptorTests {
345345
}
346346

347347
@Test
348-
func `claude model scoped widget usage toggle is default on and independent`() throws {
348+
func `claude model scoped widget usage toggle is default off and independent`() throws {
349349
let fixture = try self.makeSettingsFixture(suite: "ProviderSettingsDescriptorTests-claude-model-scoped-widget")
350350
let context = fixture.settingsContext(provider: .claude)
351351
let toggles = ClaudeProviderImplementation().settingsToggles(context: context)
352352
let widgetToggle = try #require(toggles.first {
353353
$0.id == "claude-model-scoped-weekly-usage-visible"
354354
})
355355

356-
#expect(widgetToggle.binding.wrappedValue)
356+
#expect(widgetToggle.binding.wrappedValue == false)
357357
#expect(widgetToggle.isEnabled == nil)
358358
#expect(widgetToggle.subtitle.contains("Fable"))
359359

360-
widgetToggle.binding.wrappedValue = false
361-
#expect(fixture.settings.claudeModelScopedWeeklyUsageVisible == false)
360+
widgetToggle.binding.wrappedValue = true
361+
#expect(fixture.settings.claudeModelScopedWeeklyUsageVisible)
362362

363363
fixture.settings.showOptionalCreditsAndExtraUsage = false
364364
#expect(widgetToggle.isEnabled == nil)

Tests/CodexBarTests/SettingsStoreTests.swift

Lines changed: 0 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1363,39 +1363,6 @@ struct SettingsStoreTests {
13631363
#expect(reloaded.codexSparkUsageVisible == false)
13641364
}
13651365

1366-
@Test
1367-
func `Claude model scoped widget usage defaults on persists and refreshes only menus`() async throws {
1368-
let suite = "SettingsStoreTests-claude-model-scoped-widget-usage-visible"
1369-
let defaults = try #require(UserDefaults(suiteName: suite))
1370-
defaults.removePersistentDomain(forName: suite)
1371-
let configStore = testConfigStore(suiteName: suite)
1372-
let store = SettingsStore(
1373-
userDefaults: defaults,
1374-
configStore: configStore,
1375-
zaiTokenStore: NoopZaiTokenStore(),
1376-
syntheticTokenStore: NoopSyntheticTokenStore())
1377-
1378-
#expect(store.claudeModelScopedWeeklyUsageVisible)
1379-
let backgroundRevision = store.backgroundWorkSettingsRevision
1380-
let menuDidChange = ObservationFlag()
1381-
withObservationTracking {
1382-
_ = store.menuObservationToken
1383-
} onChange: {
1384-
menuDidChange.set()
1385-
}
1386-
store.claudeModelScopedWeeklyUsageVisible = false
1387-
try? await Task.sleep(nanoseconds: 50_000_000)
1388-
#expect(store.backgroundWorkSettingsRevision == backgroundRevision)
1389-
#expect(menuDidChange.get())
1390-
1391-
let reloaded = SettingsStore(
1392-
userDefaults: defaults,
1393-
configStore: configStore,
1394-
zaiTokenStore: NoopZaiTokenStore(),
1395-
syntheticTokenStore: NoopSyntheticTokenStore())
1396-
#expect(reloaded.claudeModelScopedWeeklyUsageVisible == false)
1397-
}
1398-
13991366
@Test
14001367
func `menu observation token updates on defaults change`() async throws {
14011368
let suite = "SettingsStoreTests-observation-defaults"

Tests/CodexBarTests/UsageStoreWidgetSnapshotTests.swift

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ struct UsageStoreWidgetSnapshotTests {
122122
}
123123

124124
@Test
125-
func `widget snapshot includes known Claude model scoped weekly quotas by default`() async throws {
125+
func `widget snapshot includes known Claude model scoped weekly quotas only when enabled`() async throws {
126126
let suite = "UsageStoreWidgetSnapshotTests-claude-model-scoped-weekly-rows"
127127
let defaults = try #require(UserDefaults(suiteName: suite))
128128
defaults.removePersistentDomain(forName: suite)
@@ -164,6 +164,15 @@ struct UsageStoreWidgetSnapshotTests {
164164
store._test_widgetSnapshotSaveOverride = { widgetSnapshots.append($0) }
165165
defer { store._test_widgetSnapshotSaveOverride = nil }
166166

167+
// The setting is opt-in, so untouched defaults must project the standard quota lanes only.
168+
#expect(settings.claudeModelScopedWeeklyUsageVisible == false)
169+
store.persistWidgetSnapshot(reason: "claude-model-scoped-weekly-rows-default-test")
170+
await store.widgetSnapshotPersistTask?.value
171+
172+
let defaultEntry = try #require(widgetSnapshots.last?.entries.first { $0.provider == .claude })
173+
#expect(defaultEntry.usageRows?.map(\.id) == ["primary", "secondary"])
174+
175+
settings.claudeModelScopedWeeklyUsageVisible = true
167176
store.persistWidgetSnapshot(reason: "claude-model-scoped-weekly-rows-test")
168177
await store.widgetSnapshotPersistTask?.value
169178

@@ -180,6 +189,94 @@ struct UsageStoreWidgetSnapshotTests {
180189
#expect(hiddenEntry.usageRows?.map(\.id) == ["primary", "secondary"])
181190
}
182191

192+
@Test
193+
func `widget snapshot drops persisted Claude model scoped weekly rows once the setting is off`() async throws {
194+
let suite = "UsageStoreWidgetSnapshotTests-claude-model-scoped-weekly-fallback"
195+
let defaults = try #require(UserDefaults(suiteName: suite))
196+
defaults.removePersistentDomain(forName: suite)
197+
198+
let settings = SettingsStore(
199+
userDefaults: defaults,
200+
configStore: testConfigStore(suiteName: suite),
201+
zaiTokenStore: NoopZaiTokenStore(),
202+
syntheticTokenStore: NoopSyntheticTokenStore())
203+
settings.statusChecksEnabled = false
204+
settings.claudeModelScopedWeeklyUsageVisible = true
205+
206+
let store = UsageStore(
207+
fetcher: UsageFetcher(environment: [:]),
208+
browserDetection: BrowserDetection(cacheTTL: 0),
209+
settings: settings)
210+
let updatedAt = Date(timeIntervalSince1970: 1_800_000_000)
211+
store._setSnapshotForTesting(
212+
UsageSnapshot(
213+
primary: RateWindow(usedPercent: 25, windowMinutes: 300, resetsAt: nil, resetDescription: nil),
214+
secondary: RateWindow(usedPercent: 50, windowMinutes: 10080, resetsAt: nil, resetDescription: nil),
215+
tertiary: nil,
216+
extraRateWindows: [
217+
NamedRateWindow(
218+
id: "claude-weekly-scoped-fable",
219+
title: "Fable only",
220+
window: RateWindow(
221+
usedPercent: 30,
222+
windowMinutes: 10080,
223+
resetsAt: nil,
224+
resetDescription: nil)),
225+
],
226+
updatedAt: updatedAt),
227+
provider: .claude)
228+
229+
var widgetSnapshots: [WidgetSnapshot] = []
230+
store._test_widgetSnapshotSaveOverride = { widgetSnapshots.append($0) }
231+
defer { store._test_widgetSnapshotSaveOverride = nil }
232+
233+
store.persistWidgetSnapshot(reason: "claude-model-scoped-weekly-fallback-seed")
234+
await store.widgetSnapshotPersistTask?.value
235+
236+
let seededEntry = try #require(widgetSnapshots.last?.entries.first { $0.provider == .claude })
237+
#expect(seededEntry.usageRows?.map(\.id) == ["primary", "secondary", "claude-weekly-scoped-fable"])
238+
let quotaOwnerKey = try #require(seededEntry.quotaOwnerKey)
239+
240+
// No live Claude snapshot: the projection now falls back to the persisted rows above.
241+
store.snapshots.removeValue(forKey: .claude)
242+
settings.claudeModelScopedWeeklyUsageVisible = false
243+
store.persistWidgetSnapshot(reason: "claude-model-scoped-weekly-fallback-disabled")
244+
await store.widgetSnapshotPersistTask?.value
245+
246+
let fallbackEntry = try #require(widgetSnapshots.last?.entries.first { $0.provider == .claude })
247+
#expect(fallbackEntry.usageRows?.map(\.id) == ["primary", "secondary"])
248+
#expect(fallbackEntry.usageRows?.contains { $0.id.hasPrefix("claude-weekly-scoped-") } == false)
249+
250+
// A persisted entry whose only rows are scoped carve-outs drops out of the projection entirely.
251+
store.lastQueuedWidgetSnapshot = WidgetSnapshot(
252+
entries: [
253+
WidgetSnapshot.ProviderEntry(
254+
provider: .claude,
255+
updatedAt: updatedAt,
256+
primary: nil,
257+
secondary: nil,
258+
tertiary: nil,
259+
usageRows: [
260+
WidgetSnapshot.WidgetUsageRowSnapshot(
261+
id: "claude-weekly-scoped-fable",
262+
title: "Fable only",
263+
percentLeft: 70),
264+
],
265+
creditsRemaining: nil,
266+
codeReviewRemainingPercent: nil,
267+
tokenUsage: nil,
268+
dailyUsage: [],
269+
quotaOwnerKey: quotaOwnerKey),
270+
],
271+
enabledProviders: [.claude],
272+
generatedAt: updatedAt)
273+
274+
store.persistWidgetSnapshot(reason: "claude-model-scoped-weekly-fallback-scoped-only")
275+
await store.widgetSnapshotPersistTask?.value
276+
277+
#expect(widgetSnapshots.last?.entries.contains { $0.provider == .claude } == false)
278+
}
279+
183280
@Test
184281
func `widget snapshot includes antigravity grouped usage rows`() async throws {
185282
let suite = "UsageStoreWidgetSnapshotTests-antigravity-grouped"

0 commit comments

Comments
 (0)