Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,18 @@ struct ClaudeProviderImplementation: ProviderImplementation {
set: { context.settings.claudeSwapShowSingleAccount = $0 })

return [
ProviderSettingsToggleDescriptor(
id: "claude-model-scoped-weekly-usage-visible",
title: "Show model-specific weekly usage in widgets",
subtitle: "Shows model-specific Claude quotas, such as Fable, in desktop widgets.",
binding: context.boolBinding(\.claudeModelScopedWeeklyUsageVisible),
statusText: nil,
actions: [],
isVisible: nil,
isEnabled: nil,
onChange: nil,
onAppDidBecomeActive: nil,
onAppearWhenEnabled: nil),
ProviderSettingsToggleDescriptor(
id: "claude-daily-routines-usage-visible",
title: "Show Daily Routines usage",
Expand Down
8 changes: 8 additions & 0 deletions Sources/CodexBar/SettingsStore+Defaults.swift
Original file line number Diff line number Diff line change
Expand Up @@ -696,6 +696,14 @@ extension SettingsStore {
}
}

var claudeModelScopedWeeklyUsageVisible: Bool {
get { self.defaultsState.claudeModelScopedWeeklyUsageVisible }
set {
self.defaultsState.claudeModelScopedWeeklyUsageVisible = newValue
self.userDefaults.set(newValue, forKey: "claudeModelScopedWeeklyUsageVisible")
}
}

var codexSparkUsageVisible: Bool {
get { self.defaultsState.codexSparkUsageVisible }
set {
Expand Down
1 change: 1 addition & 0 deletions Sources/CodexBar/SettingsStore+MenuObservation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ extension SettingsStore {
_ = self.copilotBudgetExtrasEnabled
_ = self.showOptionalCreditsAndExtraUsage
_ = self.claudeDailyRoutinesUsageVisible
_ = self.claudeModelScopedWeeklyUsageVisible
_ = self.codexSparkUsageVisible
_ = self.openAIWebAccessEnabled
_ = self.openAIWebBatterySaverEnabled
Expand Down
4 changes: 4 additions & 0 deletions Sources/CodexBar/SettingsStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,9 @@ extension SettingsStore {
if Self.isRunningTests, claudeDailyRoutinesUsageVisibleDefault == nil {
userDefaults.set(true, forKey: "claudeDailyRoutinesUsageVisible")
}
// Model-scoped weekly rows are opt-in: a fresh install keeps widgets on the standard quota lanes.
let claudeModelScopedWeeklyUsageVisible = userDefaults.object(
forKey: "claudeModelScopedWeeklyUsageVisible") as? Bool ?? false
let codexSparkUsageVisibleDefault = userDefaults.object(forKey: "codexSparkUsageVisible") as? Bool
let codexSparkUsageVisible = codexSparkUsageVisibleDefault ?? true
if Self.isRunningTests, codexSparkUsageVisibleDefault == nil {
Expand Down Expand Up @@ -612,6 +615,7 @@ extension SettingsStore {
claudeWebExtrasEnabledRaw: claudeWebExtrasEnabledRaw,
showOptionalCreditsAndExtraUsage: showOptionalCreditsAndExtraUsage,
claudeDailyRoutinesUsageVisible: claudeDailyRoutinesUsageVisible,
claudeModelScopedWeeklyUsageVisible: claudeModelScopedWeeklyUsageVisible,
codexSparkUsageVisible: codexSparkUsageVisible,
openAIWebAccessEnabled: openAIWebAccessEnabled,
openAIWebBatterySaverEnabled: openAIWebBatterySaverEnabled,
Expand Down
1 change: 1 addition & 0 deletions Sources/CodexBar/SettingsStoreState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ struct SettingsDefaultsState {
var claudeWebExtrasEnabledRaw: Bool
var showOptionalCreditsAndExtraUsage: Bool
var claudeDailyRoutinesUsageVisible: Bool
var claudeModelScopedWeeklyUsageVisible: Bool
var codexSparkUsageVisible: Bool
var openAIWebAccessEnabled: Bool
var openAIWebBatterySaverEnabled: Bool
Expand Down
1 change: 1 addition & 0 deletions Sources/CodexBar/StatusItemController+WidgetSnapshot.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ extension StatusItemController {
"enabled=\(self.store.enabledProvidersForDisplay().map(\.rawValue).joined(separator: ","))",
"showUsed=\(self.settings.usageBarsShowUsed ? "1" : "0")",
"optional=\(self.settings.showOptionalCreditsAndExtraUsage ? "1" : "0")",
"claudeScopedWeekly=\(self.settings.claudeModelScopedWeeklyUsageVisible ? "1" : "0")",
].joined(separator: "|")
}

Expand Down
30 changes: 28 additions & 2 deletions Sources/CodexBar/UsageStore+WidgetSnapshot.swift
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,8 @@ extension UsageStore {
{
Self.preservedClaudeWidgetUsage(
from: previousEntry,
expectedQuotaOwnerKey: claudeQuotaOwnerKey)
expectedQuotaOwnerKey: claudeQuotaOwnerKey,
includesModelScopedWeeklyRows: self.settings.claudeModelScopedWeeklyUsageVisible)
} else {
nil
}
Expand Down Expand Up @@ -294,7 +295,8 @@ extension UsageStore {

private nonisolated static func preservedClaudeWidgetUsage(
from entry: WidgetSnapshot.ProviderEntry?,
expectedQuotaOwnerKey: String?) -> PreservedClaudeWidgetUsage?
expectedQuotaOwnerKey: String?,
includesModelScopedWeeklyRows: Bool) -> PreservedClaudeWidgetUsage?
{
guard let entry, entry.provider == .claude else { return nil }
guard let expectedQuotaOwnerKey,
Expand All @@ -309,6 +311,13 @@ extension UsageStore {
let tertiary = entry.tertiary?.isSyntheticPlaceholder == true ? nil : entry.tertiary
let usageRows = entry.usageRows?.filter { row in
guard row.window?.isSyntheticPlaceholder != true else { return false }
// Rows persisted while the setting was on must not outlive it: without a live snapshot
// this preserved list is what widgets render, so re-apply the visibility filter here.
guard includesModelScopedWeeklyRows ||
!row.id.hasPrefix(Self.claudeModelScopedWeeklyWindowIDPrefix)
else {
return false
}
return switch row.id {
case "primary": primary != nil
case "secondary": secondary != nil
Expand Down Expand Up @@ -467,6 +476,20 @@ extension UsageStore {
title: metadata?.opusLabel ?? "Opus",
percentLeft: snapshot.tertiary?.remainingPercent))
}
if provider == .claude, self.settings.claudeModelScopedWeeklyUsageVisible {
// Claude fetchers place model-scoped weekly quotas (for example, Fable) in extraRateWindows.
// Keep the widget projection generic so newly surfaced Claude model quotas appear without UI changes.
rows.append(contentsOf: (snapshot.extraRateWindows ?? []).compactMap { namedWindow in
guard namedWindow.id.hasPrefix(Self.claudeModelScopedWeeklyWindowIDPrefix),
namedWindow.usageKnown
else { return nil }
return WidgetSnapshot.WidgetUsageRowSnapshot(
id: namedWindow.id,
title: namedWindow.title,
percentLeft: namedWindow.window.remainingPercent,
window: namedWindow.window)
})
}
if provider == .kimi {
// Keep persisted widget order stable and include only Kimi's intentional subscription lanes.
let kimiWindowIDs = ["kimi-monthly", "kimi-code-7d"]
Expand All @@ -482,6 +505,9 @@ extension UsageStore {
return rows.filter { $0.percentLeft != nil }
}

/// Identifier prefix Claude fetchers use for model-scoped weekly carve-outs (for example, Fable).
private nonisolated static let claudeModelScopedWeeklyWindowIDPrefix = "claude-weekly-scoped-"

private nonisolated static let antigravityQuotaSummaryWindowIDPrefix = "antigravity-quota-summary-"
private nonisolated static let antigravityCompactFallbackWindowIDPrefix = "antigravity-compact-fallback-"

Expand Down
59 changes: 59 additions & 0 deletions Tests/CodexBarTests/ClaudeScopedWeeklySettingsTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import CodexBarCore
import Foundation
import Observation
import Testing
@testable import CodexBar

@Suite(.serialized)
@MainActor
struct ClaudeScopedWeeklySettingsTests {
private final class ObservationFlag: @unchecked Sendable {
private let lock = NSLock()
private var value = false

func set() {
self.lock.lock()
self.value = true
self.lock.unlock()
}

func get() -> Bool {
self.lock.lock()
defer { self.lock.unlock() }
return self.value
}
}

@Test
func `Claude model scoped widget usage defaults off persists and refreshes only menus`() async throws {
let suite = "ClaudeScopedWeeklySettingsTests-claude-model-scoped-widget-usage-visible"
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())

#expect(store.claudeModelScopedWeeklyUsageVisible == false)
let backgroundRevision = store.backgroundWorkSettingsRevision
let menuDidChange = ObservationFlag()
withObservationTracking {
_ = store.menuObservationToken
} onChange: {
menuDidChange.set()
}
store.claudeModelScopedWeeklyUsageVisible = true
try? await Task.sleep(nanoseconds: 50_000_000)
#expect(store.backgroundWorkSettingsRevision == backgroundRevision)
#expect(menuDidChange.get())

let reloaded = SettingsStore(
userDefaults: defaults,
configStore: configStore,
zaiTokenStore: NoopZaiTokenStore(),
syntheticTokenStore: NoopSyntheticTokenStore())
#expect(reloaded.claudeModelScopedWeeklyUsageVisible)
}
}
35 changes: 35 additions & 0 deletions Tests/CodexBarTests/ClaudeScopedWeeklyWidgetRowTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import Foundation
import Testing
@testable import CodexBarCore
@testable import CodexBarWidget

struct ClaudeScopedWeeklyWidgetRowTests {
@Test
func `Claude widget renders projected model scoped weekly rows`() {
let entry = WidgetSnapshot.ProviderEntry(
provider: .claude,
updatedAt: Date(),
primary: nil,
secondary: nil,
tertiary: nil,
usageRows: [
WidgetSnapshot.WidgetUsageRowSnapshot(id: "primary", title: "Session", percentLeft: 75),
WidgetSnapshot.WidgetUsageRowSnapshot(id: "secondary", title: "Weekly", percentLeft: 50),
WidgetSnapshot.WidgetUsageRowSnapshot(
id: "claude-weekly-scoped-fable",
title: "Fable only",
percentLeft: 70),
],
creditsRemaining: nil,
codeReviewRemainingPercent: nil,
tokenUsage: nil,
dailyUsage: [])

#expect(WidgetUsageRow.smallWidgetRowLimit(for: entry) == nil)
#expect(WidgetUsageRow.rows(for: entry).map(\.id) == [
"primary",
"secondary",
"claude-weekly-scoped-fable",
])
}
}
20 changes: 20 additions & 0 deletions Tests/CodexBarTests/ProviderSettingsDescriptorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,26 @@ struct ProviderSettingsDescriptorTests {
#expect(routinesToggle.isEnabled?() == false)
}

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

#expect(widgetToggle.binding.wrappedValue == false)
#expect(widgetToggle.isEnabled == nil)
#expect(widgetToggle.subtitle.contains("Fable"))

widgetToggle.binding.wrappedValue = true
#expect(fixture.settings.claudeModelScopedWeeklyUsageVisible)

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

@Test
func `claude single swap account toggle persists and follows integration visibility`() throws {
let fixture = try self.makeSettingsFixture(suite: "ProviderSettingsDescriptorTests-claude-swap-single")
Expand Down
Loading