From 3017da8f538240b46fda2f35ef61aff5b4e7f67d Mon Sep 17 00:00:00 2001 From: Vit129 Date: Sun, 19 Jul 2026 08:47:24 +0700 Subject: [PATCH 1/6] feat: make merged-Overview provider limit user-configurable Was hardcoded to 3 (docs/ui.md, issue #2107). Now backed by UserDefaults (static var, since it's used as a default-parameter value elsewhere and Swift disallows self in those) with a Settings stepper (1-12, default 3 for no behavior change). --- Sources/CodexBar/PreferencesMenuBarPane.swift | 15 ++++++++++++++- Sources/CodexBar/SettingsStore.swift | 17 ++++++++++++++++- .../CodexBar/StatusItemController+Menu.swift | 2 +- 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/Sources/CodexBar/PreferencesMenuBarPane.swift b/Sources/CodexBar/PreferencesMenuBarPane.swift index 975fe4f65e..f1eba5ce74 100644 --- a/Sources/CodexBar/PreferencesMenuBarPane.swift +++ b/Sources/CodexBar/PreferencesMenuBarPane.swift @@ -3,7 +3,9 @@ import SwiftUI @MainActor struct MenuBarPane: View { - private static let maxOverviewProviders = SettingsStore.mergedOverviewProviderLimit + private static var maxOverviewProviders: Int { SettingsStore.mergedOverviewProviderLimit } + + @AppStorage("mergedOverviewProviderLimit") private var overviewProviderLimitSetting = 6 @State private var isOverviewProviderPopoverPresented = false @Bindable var settings: SettingsStore @@ -75,6 +77,17 @@ struct MenuBarPane: View { self.overviewProviderRow .disabled(!self.settings.mergeIcons) + + Stepper( + value: self.$overviewProviderLimitSetting, + in: 1 ... 12) { + SettingsRowLabel( + "Overview tab limit", + subtitle: "How many providers can show at once (default 6).") + Text("\(self.overviewProviderLimitSetting)") + .foregroundStyle(.secondary) + } + .disabled(!self.settings.mergeIcons) } header: { Text(L("section_combined_icon")) } diff --git a/Sources/CodexBar/SettingsStore.swift b/Sources/CodexBar/SettingsStore.swift index 62e5dc17e1..fc25401d97 100644 --- a/Sources/CodexBar/SettingsStore.swift +++ b/Sources/CodexBar/SettingsStore.swift @@ -188,7 +188,22 @@ enum CodexAccountMenuProjectionRevalidationResult: Equatable { @Observable final class SettingsStore { static let sharedDefaults = AppGroupSupport.sharedDefaults() - static let mergedOverviewProviderLimit = 6 + /// User-configurable cap on how many providers render in the merged-icon + /// Overview tab. Used as a default-parameter value elsewhere in this file, + /// which Swift requires to be self-independent — hence `static var` (backed + /// by UserDefaults.standard, the same store `@AppStorage` targets) instead + /// of an instance property on `defaultsState`. Default matches the + /// original hardcoded value so existing users see no behavior change + /// unless they raise it. + static var mergedOverviewProviderLimit: Int { + get { + let stored = UserDefaults.standard.integer(forKey: "mergedOverviewProviderLimit") + return stored > 0 ? stored : 6 + } + set { + UserDefaults.standard.set(max(1, newValue), forKey: "mergedOverviewProviderLimit") + } + } static let productionCodexAccountReconciliationSnapshotCacheInterval: TimeInterval = 2 static let isRunningTests: Bool = { let env = ProcessInfo.processInfo.environment diff --git a/Sources/CodexBar/StatusItemController+Menu.swift b/Sources/CodexBar/StatusItemController+Menu.swift index f5d0eaa43c..5081cee1fe 100644 --- a/Sources/CodexBar/StatusItemController+Menu.swift +++ b/Sources/CodexBar/StatusItemController+Menu.swift @@ -8,7 +8,7 @@ import SwiftUI extension StatusItemController { static let menuCardBaseWidth: CGFloat = 310 - private static let maxOverviewProviders = SettingsStore.mergedOverviewProviderLimit + private static var maxOverviewProviders: Int { SettingsStore.mergedOverviewProviderLimit } static let overviewRowIdentifierPrefix = "overviewRow-" static let persistentRefreshMenuItemID = "persistentRefreshAction" private static let defaultMenuOpenRefreshDelay: Duration = .seconds(1.2) From 86c6c55e0ad8e0b914473b5a041d2a98c4691d5f Mon Sep 17 00:00:00 2001 From: Vit129 Date: Sat, 1 Aug 2026 20:50:14 +0700 Subject: [PATCH 2/6] fix: refresh already-open Overview menu when the provider-limit stepper changes Addresses clawsweeper P2 finding on #2339: the Overview-limit Stepper wrote straight to UserDefaults via @AppStorage, bypassing SettingsStore entirely, so menuObservationToken (which drives cached-menu invalidation) never changed and an already-open Overview menu kept its old row count. Adds mergedOverviewProviderLimitRevision, an @Observable-tracked instance counter bumped by a new instance accessor for the setting, and touches it in menuObservationToken. The Stepper now binds through settings.mergedOverviewProviderLimit instead of a raw @AppStorage. --- Sources/CodexBar/PreferencesMenuBarPane.swift | 6 ++---- Sources/CodexBar/SettingsStore+Defaults.swift | 10 ++++++++++ Sources/CodexBar/SettingsStore+MenuObservation.swift | 1 + Sources/CodexBar/SettingsStore.swift | 4 ++++ 4 files changed, 17 insertions(+), 4 deletions(-) diff --git a/Sources/CodexBar/PreferencesMenuBarPane.swift b/Sources/CodexBar/PreferencesMenuBarPane.swift index f1eba5ce74..5c198d1064 100644 --- a/Sources/CodexBar/PreferencesMenuBarPane.swift +++ b/Sources/CodexBar/PreferencesMenuBarPane.swift @@ -5,8 +5,6 @@ import SwiftUI struct MenuBarPane: View { private static var maxOverviewProviders: Int { SettingsStore.mergedOverviewProviderLimit } - @AppStorage("mergedOverviewProviderLimit") private var overviewProviderLimitSetting = 6 - @State private var isOverviewProviderPopoverPresented = false @Bindable var settings: SettingsStore @Bindable var store: UsageStore @@ -79,12 +77,12 @@ struct MenuBarPane: View { .disabled(!self.settings.mergeIcons) Stepper( - value: self.$overviewProviderLimitSetting, + value: self.$settings.mergedOverviewProviderLimit, in: 1 ... 12) { SettingsRowLabel( "Overview tab limit", subtitle: "How many providers can show at once (default 6).") - Text("\(self.overviewProviderLimitSetting)") + Text("\(self.settings.mergedOverviewProviderLimit)") .foregroundStyle(.secondary) } .disabled(!self.settings.mergeIcons) diff --git a/Sources/CodexBar/SettingsStore+Defaults.swift b/Sources/CodexBar/SettingsStore+Defaults.swift index 0624723be2..a20b0d81e8 100644 --- a/Sources/CodexBar/SettingsStore+Defaults.swift +++ b/Sources/CodexBar/SettingsStore+Defaults.swift @@ -814,6 +814,16 @@ extension SettingsStore { self.mergedOverviewSelectionEditedActiveProvidersRaw = nil } + /// Instance accessor for `mergedOverviewProviderLimit` — bumps `mergedOverviewProviderLimitRevision` + /// so an already-open Overview menu observing this store picks up the change. + var mergedOverviewProviderLimit: Int { + get { Self.mergedOverviewProviderLimit } + set { + Self.mergedOverviewProviderLimit = newValue + self.mergedOverviewProviderLimitRevision &+= 1 + } + } + func resolvedMergedOverviewProviders( activeProviders: [UsageProvider], maxVisibleProviders: Int = SettingsStore.mergedOverviewProviderLimit) -> [UsageProvider] diff --git a/Sources/CodexBar/SettingsStore+MenuObservation.swift b/Sources/CodexBar/SettingsStore+MenuObservation.swift index c8e05dfd69..04421bdffe 100644 --- a/Sources/CodexBar/SettingsStore+MenuObservation.swift +++ b/Sources/CodexBar/SettingsStore+MenuObservation.swift @@ -84,6 +84,7 @@ extension SettingsStore { _ = self.mergeIcons _ = self.switcherShowsIcons _ = self.mergedOverviewSelectedProviders + _ = self.mergedOverviewProviderLimitRevision _ = self.zaiAPIToken _ = self.syntheticAPIToken _ = self.codexCookieHeader diff --git a/Sources/CodexBar/SettingsStore.swift b/Sources/CodexBar/SettingsStore.swift index fc25401d97..0ec77fb9d7 100644 --- a/Sources/CodexBar/SettingsStore.swift +++ b/Sources/CodexBar/SettingsStore.swift @@ -204,6 +204,10 @@ final class SettingsStore { UserDefaults.standard.set(max(1, newValue), forKey: "mergedOverviewProviderLimit") } } + /// Bumped whenever `mergedOverviewProviderLimit` changes through the instance accessor below, + /// so `menuObservationToken` can pick up the change on an already-open Overview menu — the + /// static var itself isn't `@Observable`-tracked. + var mergedOverviewProviderLimitRevision = 0 static let productionCodexAccountReconciliationSnapshotCacheInterval: TimeInterval = 2 static let isRunningTests: Bool = { let env = ProcessInfo.processInfo.environment From 82827554f929a8dc1386dbfc5c31b2b065b10d2a Mon Sep 17 00:00:00 2001 From: Vit129 Date: Sat, 1 Aug 2026 21:35:11 +0700 Subject: [PATCH 3/6] fix: persist merged-Overview provider limit through SettingsStore.userDefaults Addresses clawsweeper P2 finding on #2339: mergedOverviewProviderLimit read and wrote UserDefaults.standard directly instead of the store's own configured userDefaults, so isolated test stores (and any future non-standard suite) couldn't observe or reset it independently of the real app. Replaces the static get/set with a plain mergedOverviewProviderLimitDefault constant (used only where Swift requires a self-independent default parameter value) and routes every real read/write through the existing mergedOverviewProviderLimit instance accessor, backed by self.userDefaults like the rest of SettingsStore's persisted settings. --- Sources/CodexBar/PreferencesMenuBarPane.swift | 14 +++++------ Sources/CodexBar/SettingsStore+Defaults.swift | 23 +++++++++++-------- Sources/CodexBar/SettingsStore.swift | 21 ++++------------- .../StatusItemController+Animation.swift | 4 ++-- ...tatusItemController+CountdownRefresh.swift | 2 +- .../CodexBar/StatusItemController+Menu.swift | 7 +++--- ...tatusItemController+MenuLocalization.swift | 2 +- ...ItemController+PersistentMenuActions.swift | 2 +- ...tusItemController+ProviderNavigation.swift | 2 +- .../PreferencesPaneSmokeTests.swift | 2 +- 10 files changed, 35 insertions(+), 44 deletions(-) diff --git a/Sources/CodexBar/PreferencesMenuBarPane.swift b/Sources/CodexBar/PreferencesMenuBarPane.swift index 5c198d1064..86d5327944 100644 --- a/Sources/CodexBar/PreferencesMenuBarPane.swift +++ b/Sources/CodexBar/PreferencesMenuBarPane.swift @@ -3,13 +3,11 @@ import SwiftUI @MainActor struct MenuBarPane: View { - private static var maxOverviewProviders: Int { SettingsStore.mergedOverviewProviderLimit } - @State private var isOverviewProviderPopoverPresented = false @Bindable var settings: SettingsStore @Bindable var store: UsageStore - static func overviewProviderLimitText(limit: Int = Self.maxOverviewProviders) -> String { + static func overviewProviderLimitText(limit: Int) -> String { L("overview_choose_providers", String(limit)) } @@ -146,7 +144,7 @@ struct MenuBarPane: View { private var overviewProviderPopover: some View { VStack(alignment: .leading, spacing: 10) { - Text(Self.overviewProviderLimitText()) + Text(Self.overviewProviderLimitText(limit: self.settings.mergedOverviewProviderLimit)) .font(.headline) Text(L("overview_rows_follow_order")) .font(.footnote) @@ -167,7 +165,7 @@ struct MenuBarPane: View { .toggleStyle(.checkbox) .disabled( !self.overviewSelectedProviders.contains(provider) && - self.overviewSelectedProviders.count >= Self.maxOverviewProviders) + self.overviewSelectedProviders.count >= self.settings.mergedOverviewProviderLimit) } } } @@ -184,7 +182,7 @@ struct MenuBarPane: View { private var overviewSelectedProviders: [UsageProvider] { self.settings.resolvedMergedOverviewProviders( activeProviders: self.activeProvidersInOrder, - maxVisibleProviders: Self.maxOverviewProviders) + maxVisibleProviders: self.settings.mergedOverviewProviderLimit) } private var showsOverviewConfigureButton: Bool { @@ -206,12 +204,12 @@ struct MenuBarPane: View { provider: provider, isSelected: isSelected, activeProviders: self.activeProvidersInOrder, - maxVisibleProviders: Self.maxOverviewProviders) + maxVisibleProviders: self.settings.mergedOverviewProviderLimit) } private func reconcileOverviewSelection() { _ = self.settings.reconcileMergedOverviewSelectedProviders( activeProviders: self.activeProvidersInOrder, - maxVisibleProviders: Self.maxOverviewProviders) + maxVisibleProviders: self.settings.mergedOverviewProviderLimit) } } diff --git a/Sources/CodexBar/SettingsStore+Defaults.swift b/Sources/CodexBar/SettingsStore+Defaults.swift index a20b0d81e8..7e069e558d 100644 --- a/Sources/CodexBar/SettingsStore+Defaults.swift +++ b/Sources/CodexBar/SettingsStore+Defaults.swift @@ -771,10 +771,10 @@ extension SettingsStore { get { Self.decodeProviders( self.mergedOverviewSelectedProvidersRaw, - maxCount: Self.mergedOverviewProviderLimit) + maxCount: self.mergedOverviewProviderLimit) } set { - let normalized = Self.normalizeProviders(newValue, maxCount: Self.mergedOverviewProviderLimit) + let normalized = Self.normalizeProviders(newValue, maxCount: self.mergedOverviewProviderLimit) self.mergedOverviewSelectedProvidersRaw = normalized.map(\.rawValue) } } @@ -814,19 +814,24 @@ extension SettingsStore { self.mergedOverviewSelectionEditedActiveProvidersRaw = nil } - /// Instance accessor for `mergedOverviewProviderLimit` — bumps `mergedOverviewProviderLimitRevision` - /// so an already-open Overview menu observing this store picks up the change. + /// User-configurable cap on how many providers render in the merged-icon Overview tab, + /// persisted through this store's own `userDefaults` (not `.standard` directly) so isolated + /// test stores can observe and reset it independently. Bumps + /// `mergedOverviewProviderLimitRevision` so an already-open Overview menu picks up the change. var mergedOverviewProviderLimit: Int { - get { Self.mergedOverviewProviderLimit } + get { + let stored = self.userDefaults.integer(forKey: "mergedOverviewProviderLimit") + return stored > 0 ? stored : Self.mergedOverviewProviderLimitDefault + } set { - Self.mergedOverviewProviderLimit = newValue + self.userDefaults.set(max(1, newValue), forKey: "mergedOverviewProviderLimit") self.mergedOverviewProviderLimitRevision &+= 1 } } func resolvedMergedOverviewProviders( activeProviders: [UsageProvider], - maxVisibleProviders: Int = SettingsStore.mergedOverviewProviderLimit) -> [UsageProvider] + maxVisibleProviders: Int = SettingsStore.mergedOverviewProviderLimitDefault) -> [UsageProvider] { guard maxVisibleProviders > 0 else { return [] } let normalizedActive = Self.normalizeProviders(activeProviders) @@ -846,7 +851,7 @@ extension SettingsStore { @discardableResult func reconcileMergedOverviewSelectedProviders( activeProviders: [UsageProvider], - maxVisibleProviders: Int = SettingsStore.mergedOverviewProviderLimit) -> [UsageProvider] + maxVisibleProviders: Int = SettingsStore.mergedOverviewProviderLimitDefault) -> [UsageProvider] { guard maxVisibleProviders > 0 else { self.clearMergedOverviewSelectionPreference() @@ -883,7 +888,7 @@ extension SettingsStore { provider: UsageProvider, isSelected: Bool, activeProviders: [UsageProvider], - maxVisibleProviders: Int = SettingsStore.mergedOverviewProviderLimit) -> [UsageProvider] + maxVisibleProviders: Int = SettingsStore.mergedOverviewProviderLimitDefault) -> [UsageProvider] { guard maxVisibleProviders > 0 else { self.clearMergedOverviewSelectionPreference() diff --git a/Sources/CodexBar/SettingsStore.swift b/Sources/CodexBar/SettingsStore.swift index 0ec77fb9d7..0850ddcf08 100644 --- a/Sources/CodexBar/SettingsStore.swift +++ b/Sources/CodexBar/SettingsStore.swift @@ -188,22 +188,11 @@ enum CodexAccountMenuProjectionRevalidationResult: Equatable { @Observable final class SettingsStore { static let sharedDefaults = AppGroupSupport.sharedDefaults() - /// User-configurable cap on how many providers render in the merged-icon - /// Overview tab. Used as a default-parameter value elsewhere in this file, - /// which Swift requires to be self-independent — hence `static var` (backed - /// by UserDefaults.standard, the same store `@AppStorage` targets) instead - /// of an instance property on `defaultsState`. Default matches the - /// original hardcoded value so existing users see no behavior change - /// unless they raise it. - static var mergedOverviewProviderLimit: Int { - get { - let stored = UserDefaults.standard.integer(forKey: "mergedOverviewProviderLimit") - return stored > 0 ? stored : 6 - } - set { - UserDefaults.standard.set(max(1, newValue), forKey: "mergedOverviewProviderLimit") - } - } + /// Fallback used only by default-parameter expressions below, which Swift requires to be + /// self-independent (no access to `self.userDefaults`). Every real call site instead reads + /// the persisted, per-store value through the `mergedOverviewProviderLimit` instance accessor + /// in SettingsStore+Defaults.swift, so isolated test stores observe and reset it correctly. + static let mergedOverviewProviderLimitDefault = 6 /// Bumped whenever `mergedOverviewProviderLimit` changes through the instance accessor below, /// so `menuObservationToken` can pick up the change on an already-open Overview menu — the /// static var itself isn't `@Observable`-tracked. diff --git a/Sources/CodexBar/StatusItemController+Animation.swift b/Sources/CodexBar/StatusItemController+Animation.swift index d2efd57f76..366009ea76 100644 --- a/Sources/CodexBar/StatusItemController+Animation.swift +++ b/Sources/CodexBar/StatusItemController+Animation.swift @@ -1358,7 +1358,7 @@ extension StatusItemController { let activeProviders = self.store.enabledProvidersForDisplay() let overviewProviders = self.settings.resolvedMergedOverviewProviders( activeProviders: activeProviders, - maxVisibleProviders: SettingsStore.mergedOverviewProviderLimit) + maxVisibleProviders: self.settings.mergedOverviewProviderLimit) if let highest = self.store.providerWithHighestUsage(candidateProviders: overviewProviders) { return highest.provider } @@ -1372,7 +1372,7 @@ extension StatusItemController { let enabledProviders = self.store.enabledProvidersForDisplay() let overviewProviders = self.settings.resolvedMergedOverviewProviders( activeProviders: enabledProviders, - maxVisibleProviders: SettingsStore.mergedOverviewProviderLimit) + maxVisibleProviders: self.settings.mergedOverviewProviderLimit) if let provider = overviewProviders.first(where: { self.store.isEnabled($0) }) { return provider } diff --git a/Sources/CodexBar/StatusItemController+CountdownRefresh.swift b/Sources/CodexBar/StatusItemController+CountdownRefresh.swift index 7fa38c6071..ac0d33a9a2 100644 --- a/Sources/CodexBar/StatusItemController+CountdownRefresh.swift +++ b/Sources/CodexBar/StatusItemController+CountdownRefresh.swift @@ -128,7 +128,7 @@ extension StatusItemController { let activeProviders = self.store.enabledProvidersForDisplay() return self.settings.resolvedMergedOverviewProviders( activeProviders: activeProviders, - maxVisibleProviders: SettingsStore.mergedOverviewProviderLimit).contains(.codex) + maxVisibleProviders: self.settings.mergedOverviewProviderLimit).contains(.codex) } func observeMenuBarTimeEnvironmentChanges() { diff --git a/Sources/CodexBar/StatusItemController+Menu.swift b/Sources/CodexBar/StatusItemController+Menu.swift index 5081cee1fe..c41ae03e93 100644 --- a/Sources/CodexBar/StatusItemController+Menu.swift +++ b/Sources/CodexBar/StatusItemController+Menu.swift @@ -8,7 +8,6 @@ import SwiftUI extension StatusItemController { static let menuCardBaseWidth: CGFloat = 310 - private static var maxOverviewProviders: Int { SettingsStore.mergedOverviewProviderLimit } static let overviewRowIdentifierPrefix = "overviewRow-" static let persistentRefreshMenuItemID = "persistentRefreshAction" private static let defaultMenuOpenRefreshDelay: Duration = .seconds(1.2) @@ -598,7 +597,7 @@ extension StatusItemController { private func addOverviewEmptyState(to menu: NSMenu, enabledProviders: [UsageProvider]) { let resolvedProviders = self.settings.resolvedMergedOverviewProviders( activeProviders: enabledProviders, - maxVisibleProviders: Self.maxOverviewProviders) + maxVisibleProviders: self.settings.mergedOverviewProviderLimit) let message = resolvedProviders.isEmpty ? L("No providers selected for Overview.") : L("No overview data available.") @@ -1102,7 +1101,7 @@ extension StatusItemController { private func includesOverviewTab(enabledProviders: [UsageProvider]) -> Bool { !self.settings.resolvedMergedOverviewProviders( activeProviders: enabledProviders, - maxVisibleProviders: Self.maxOverviewProviders).isEmpty + maxVisibleProviders: self.settings.mergedOverviewProviderLimit).isEmpty } private func resolvedSwitcherSelection( @@ -1240,7 +1239,7 @@ extension StatusItemController { { return self.settings.resolvedMergedOverviewProviders( activeProviders: enabledProviders, - maxVisibleProviders: Self.maxOverviewProviders) + maxVisibleProviders: self.settings.mergedOverviewProviderLimit) } if let provider = self.menuProvider(for: menu) diff --git a/Sources/CodexBar/StatusItemController+MenuLocalization.swift b/Sources/CodexBar/StatusItemController+MenuLocalization.swift index abad4320d1..dc10970982 100644 --- a/Sources/CodexBar/StatusItemController+MenuLocalization.swift +++ b/Sources/CodexBar/StatusItemController+MenuLocalization.swift @@ -33,6 +33,6 @@ extension StatusItemController { private func includesOverviewTab(for providers: [UsageProvider]) -> Bool { !self.settings.resolvedMergedOverviewProviders( activeProviders: providers, - maxVisibleProviders: SettingsStore.mergedOverviewProviderLimit).isEmpty + maxVisibleProviders: self.settings.mergedOverviewProviderLimit).isEmpty } } diff --git a/Sources/CodexBar/StatusItemController+PersistentMenuActions.swift b/Sources/CodexBar/StatusItemController+PersistentMenuActions.swift index bf3db110d3..4538b0ddfe 100644 --- a/Sources/CodexBar/StatusItemController+PersistentMenuActions.swift +++ b/Sources/CodexBar/StatusItemController+PersistentMenuActions.swift @@ -54,7 +54,7 @@ extension StatusItemController { if let mergedMenu = self.mergedMenu, menu !== mergedMenu { return false } let providers = self.settings.resolvedMergedOverviewProviders( activeProviders: self.store.enabledProvidersForDisplay(), - maxVisibleProviders: SettingsStore.mergedOverviewProviderLimit) + maxVisibleProviders: self.settings.mergedOverviewProviderLimit) return !providers.isEmpty && self.settings.mergedMenuLastSelectedWasOverview } } diff --git a/Sources/CodexBar/StatusItemController+ProviderNavigation.swift b/Sources/CodexBar/StatusItemController+ProviderNavigation.swift index cc4c71e653..4e5057c720 100644 --- a/Sources/CodexBar/StatusItemController+ProviderNavigation.swift +++ b/Sources/CodexBar/StatusItemController+ProviderNavigation.swift @@ -45,7 +45,7 @@ extension StatusItemController { let includesOverview = !self.settings.resolvedMergedOverviewProviders( activeProviders: enabledProviders, - maxVisibleProviders: SettingsStore.mergedOverviewProviderLimit).isEmpty + maxVisibleProviders: self.settings.mergedOverviewProviderLimit).isEmpty var selections = enabledProviders.map(ProviderSwitcherSelection.provider) if includesOverview { selections.insert(.overview, at: 0) diff --git a/Tests/CodexBarTests/PreferencesPaneSmokeTests.swift b/Tests/CodexBarTests/PreferencesPaneSmokeTests.swift index d02f09fe57..3126500b3d 100644 --- a/Tests/CodexBarTests/PreferencesPaneSmokeTests.swift +++ b/Tests/CodexBarTests/PreferencesPaneSmokeTests.swift @@ -118,7 +118,7 @@ struct PreferencesPaneSmokeTests { @Test func `overview provider limit text shows the configured maximum`() { - let text = MenuBarPane.overviewProviderLimitText() + let text = MenuBarPane.overviewProviderLimitText(limit: SettingsStore.mergedOverviewProviderLimitDefault) #expect(text.contains("6")) #expect(!text.contains("%@")) From 12be1949f7ff42acfd7bb668c0ecba87d2977d40 Mon Sep 17 00:00:00 2001 From: Vit129 Date: Sun, 2 Aug 2026 13:28:37 +0700 Subject: [PATCH 4/6] style: fix swiftformat brace-wrap violation in PreferencesMenuBarPane CI lint gate flagged spaceAroundOperators/wrapMultilineStatementBraces on the overview-limit Stepper introduced in this PR. --- Sources/CodexBar/PreferencesMenuBarPane.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/CodexBar/PreferencesMenuBarPane.swift b/Sources/CodexBar/PreferencesMenuBarPane.swift index 86d5327944..29ce7acf2c 100644 --- a/Sources/CodexBar/PreferencesMenuBarPane.swift +++ b/Sources/CodexBar/PreferencesMenuBarPane.swift @@ -76,7 +76,8 @@ struct MenuBarPane: View { Stepper( value: self.$settings.mergedOverviewProviderLimit, - in: 1 ... 12) { + in: 1...12) + { SettingsRowLabel( "Overview tab limit", subtitle: "How many providers can show at once (default 6).") From feafb639317fd70069f2d45d71dd79072b220865 Mon Sep 17 00:00:00 2001 From: Vit129 Date: Sun, 2 Aug 2026 19:33:43 +0700 Subject: [PATCH 5/6] fix: localize Overview tab limit settings copy The stepper's title/subtitle were raw English strings while every adjacent Menu Bar settings row uses L(...) (ClawSweeper finding on PR #2339). --- Sources/CodexBar/PreferencesMenuBarPane.swift | 4 ++-- Sources/CodexBar/Resources/en.lproj/Localizable.strings | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Sources/CodexBar/PreferencesMenuBarPane.swift b/Sources/CodexBar/PreferencesMenuBarPane.swift index 29ce7acf2c..cb61a76977 100644 --- a/Sources/CodexBar/PreferencesMenuBarPane.swift +++ b/Sources/CodexBar/PreferencesMenuBarPane.swift @@ -79,8 +79,8 @@ struct MenuBarPane: View { in: 1...12) { SettingsRowLabel( - "Overview tab limit", - subtitle: "How many providers can show at once (default 6).") + L("overview_provider_limit_title"), + subtitle: L("overview_provider_limit_subtitle")) Text("\(self.settings.mergedOverviewProviderLimit)") .foregroundStyle(.secondary) } diff --git a/Sources/CodexBar/Resources/en.lproj/Localizable.strings b/Sources/CodexBar/Resources/en.lproj/Localizable.strings index 5cc6b2defb..06c6df6ef5 100644 --- a/Sources/CodexBar/Resources/en.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/en.lproj/Localizable.strings @@ -583,6 +583,8 @@ "show_most_used_provider_subtitle" = "Menu bar auto-shows the provider closest to its rate limit."; "display_mode_title" = "Display mode"; "display_mode_subtitle" = "Choose what to show in the menu bar (Pace shows usage vs. expected)."; +"overview_provider_limit_title" = "Overview tab limit"; +"overview_provider_limit_subtitle" = "How many providers can show at once (default 6)."; "show_quota_warning_markers_title" = "Show quota warning markers"; "show_quota_warning_markers_subtitle" = "Draw threshold tick marks on usage bars when quota warnings are configured."; "weekly_progress_work_days_title" = "Work days"; From 328bc81a9cf94a31c52713cb10a227e464791869 Mon Sep 17 00:00:00 2001 From: Vit129 Date: Sun, 2 Aug 2026 21:18:20 +0700 Subject: [PATCH 6/6] fix: make Overview tab limit stepper actually respond to clicks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Manual testing found the stepper completely unresponsive. Root cause: mergedOverviewProviderLimit's getter read straight from self.userDefaults, which is @ObservationIgnored — so SwiftUI recorded zero dependencies for that read and never re-rendered the Stepper/Text after a click updated the stored value. Reading the already-tracked mergedOverviewProviderLimitRevision first gives the getter an observable dependency, same technique already used in menuObservationToken. Also moves the stepper's numeric readout out of the Stepper's own label into a LabeledContent accessory (matching CostHistoryDaysEditor's existing pattern) so the +/- hit target isn't sharing space with a three-line label. --- Sources/CodexBar/PreferencesMenuBarPane.swift | 19 +++++++++++++------ Sources/CodexBar/SettingsStore+Defaults.swift | 5 +++++ 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/Sources/CodexBar/PreferencesMenuBarPane.swift b/Sources/CodexBar/PreferencesMenuBarPane.swift index cb61a76977..1d881733d9 100644 --- a/Sources/CodexBar/PreferencesMenuBarPane.swift +++ b/Sources/CodexBar/PreferencesMenuBarPane.swift @@ -74,15 +74,22 @@ struct MenuBarPane: View { self.overviewProviderRow .disabled(!self.settings.mergeIcons) - Stepper( - value: self.$settings.mergedOverviewProviderLimit, - in: 1...12) - { + LabeledContent { + HStack(spacing: 4) { + Text("\(self.settings.mergedOverviewProviderLimit)") + .foregroundStyle(.secondary) + Stepper( + value: self.$settings.mergedOverviewProviderLimit, + in: 1...12) + { + EmptyView() + } + .labelsHidden() + } + } label: { SettingsRowLabel( L("overview_provider_limit_title"), subtitle: L("overview_provider_limit_subtitle")) - Text("\(self.settings.mergedOverviewProviderLimit)") - .foregroundStyle(.secondary) } .disabled(!self.settings.mergeIcons) } header: { diff --git a/Sources/CodexBar/SettingsStore+Defaults.swift b/Sources/CodexBar/SettingsStore+Defaults.swift index 7e069e558d..7cf452d872 100644 --- a/Sources/CodexBar/SettingsStore+Defaults.swift +++ b/Sources/CodexBar/SettingsStore+Defaults.swift @@ -820,6 +820,11 @@ extension SettingsStore { /// `mergedOverviewProviderLimitRevision` so an already-open Overview menu picks up the change. var mergedOverviewProviderLimit: Int { get { + // `userDefaults` is `@ObservationIgnored`, so reading it directly registers no + // dependency — SwiftUI would never re-render a Stepper/Text bound to this property. + // Reading the tracked revision counter first gives the getter an observable + // dependency, matching the technique already used in menuObservationToken. + _ = self.mergedOverviewProviderLimitRevision let stored = self.userDefaults.integer(forKey: "mergedOverviewProviderLimit") return stored > 0 ? stored : Self.mergedOverviewProviderLimitDefault }