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
19 changes: 12 additions & 7 deletions Sources/CodexBar/PreferencesGeneralPane.swift
Original file line number Diff line number Diff line change
Expand Up @@ -178,13 +178,18 @@ struct GeneralPane: View {

Toggle(L("refresh_on_open_title"), isOn: self.$settings.refreshAllProvidersOnMenuOpen)

Toggle(isOn: self.$settings.backgroundWorkLowPowerModeEnabled) {
SettingsRowLabel(
L("Low Power Mode"),
subtitle: L(
"Runs automatic provider, local usage, and storage refreshes no more often than every " +
"30 minutes. Manual refresh remains available."))
}
SettingsMenuPicker(
selection: self.$settings.backgroundWorkLowPowerModePreference,
options: GeneralSettingsMenuOptions.lowPowerModePreferences,
label: {
SettingsRowLabel(
L("Low Power Mode"),
subtitle: L(
"When on, runs automatic provider, local usage, and storage refreshes no more " +
"often than every 30 minutes. Manual refresh remains available. Automatic " +
"follows the system Low Power Mode setting."))
},
optionLabel: { option in Text(option.label) })

Toggle(isOn: self.$settings.statusChecksEnabled) {
SettingsRowLabel(
Expand Down
1 change: 1 addition & 0 deletions Sources/CodexBar/PreferencesMenuPicker.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ struct SettingsMenuPicker<Value: Hashable, Label: View, OptionLabel: View>: View
enum GeneralSettingsMenuOptions {
static let languages = AppLanguage.allCases.map(\.rawValue)
static let refreshFrequencies = RefreshFrequency.allCases
static let lowPowerModePreferences = LowPowerModePreference.allCases

static func terminalApps(selected: TerminalApp) -> [TerminalApp] {
TerminalApp.pickerOptions(selected: selected)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,7 @@
"Quit CodexBar" = "退出 CodexBar";
"Random (default)" = "随机(默认)";
"Reads local usage logs. Shows today + last 30 days cost in the menu." = "读取本地用量日志。在菜单中显示今天及所选历史窗口的费用。";
"Runs automatic provider, local usage, and storage refreshes no more often than every 30 minutes. Manual refresh remains available." = "自动供应商刷新、本地用量扫描和存储扫描最多每 30 分钟运行一次;仍可随时手动刷新。";
"When on, runs automatic provider, local usage, and storage refreshes no more often than every 30 minutes. Manual refresh remains available. Automatic follows the system Low Power Mode setting." = "开启后,自动供应商刷新、本地用量扫描和存储扫描最多每 30 分钟运行一次;仍可随时手动刷新。选择“自动”时将跟随系统的低电量模式设置。";
"Refresh" = "刷新";
"Refreshing" = "正在刷新";
"Refresh cadence" = "刷新频率";
Expand Down
22 changes: 16 additions & 6 deletions Sources/CodexBar/SettingsStore+Defaults.swift
Original file line number Diff line number Diff line change
Expand Up @@ -764,18 +764,28 @@ extension SettingsStore {
}
}

var backgroundWorkLowPowerModeEnabled: Bool {
get { self.defaultsState.backgroundWorkLowPowerModeEnabled }
var backgroundWorkLowPowerModePreference: LowPowerModePreference {
get { self.defaultsState.backgroundWorkLowPowerModePreference }
set {
self.defaultsState.backgroundWorkLowPowerModeEnabled = newValue
self.userDefaults.set(newValue, forKey: "backgroundWorkLowPowerModeEnabled")
self.defaultsState.backgroundWorkLowPowerModePreference = newValue
self.userDefaults.set(newValue.rawValue, forKey: "backgroundWorkLowPowerModePreference")
CodexBarLog.logger(LogCategories.settings).info(
"Background work low power mode updated",
metadata: ["enabled": newValue ? "1" : "0"])
"Background work low power mode preference updated",
metadata: ["preference": newValue.rawValue])
self.noteBackgroundWorkSettingsChanged()
}
}

/// Resolves `backgroundWorkLowPowerModePreference` against the live system Low Power Mode state
/// when the preference is `.automatic`.
var backgroundWorkLowPowerModeEnabled: Bool {
switch self.backgroundWorkLowPowerModePreference {
case .off: false
case .on: true
case .automatic: ProcessInfo.processInfo.isLowPowerModeEnabled
Comment thread
elijahfriedman marked this conversation as resolved.
}
}

var effectiveOpenAIWebBatterySaverEnabled: Bool {
self.openAIWebBatterySaverEnabled || self.backgroundWorkLowPowerModeEnabled
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/CodexBar/SettingsStore+MenuObservation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ extension SettingsStore {
_ = self.codexSparkUsageVisible
_ = self.openAIWebAccessEnabled
_ = self.openAIWebBatterySaverEnabled
_ = self.backgroundWorkLowPowerModeEnabled
_ = self.backgroundWorkLowPowerModePreference
_ = self.providerStorageFootprintsEnabled
_ = self.agentSessionsEnabled
_ = self.agentSessionLabelStyle
Expand Down
59 changes: 56 additions & 3 deletions Sources/CodexBar/SettingsStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,24 @@ enum KiroMenuBarDisplayMode: String, CaseIterable, Identifiable {
}
}

enum LowPowerModePreference: String, CaseIterable, Identifiable {
case off
case on
case automatic

var id: String {
self.rawValue
}

var label: String {
switch self {
case .off: L("Off")
case .on: L("On")
case .automatic: L("Automatic")
}
}
}

enum MultiAccountMenuLayout: String, CaseIterable, Identifiable {
case segmented
case stacked
Expand Down Expand Up @@ -247,6 +265,7 @@ final class SettingsStore {
#endif
@ObservationIgnored var mergedMenuLastSelectedWasOverviewStorage = false
@ObservationIgnored var selectedMenuProviderRawStorage: String?
@ObservationIgnored private nonisolated(unsafe) var lowPowerModeObserver: NSObjectProtocol?
var defaultsState: SettingsDefaultsState
var configRevision: Int = 0
var providerDetailSettingsRevision: Int = 0
Expand Down Expand Up @@ -400,10 +419,31 @@ final class SettingsStore {
}
KeychainAccessGate.isDisabled = self.debugDisableKeychainAccess
self.startConfigFileWatcher()
self.observeSystemPowerStateChanges()
}

deinit {
self.configFileWatcher?.stop()
if let lowPowerModeObserver {
NotificationCenter.default.removeObserver(lowPowerModeObserver)
}
}

/// Automatic Low Power Mode reads `ProcessInfo.isLowPowerModeEnabled` live, but background
/// timers only restart when `backgroundWorkSettingsRevision` changes. Without this, toggling
/// the system's Low Power Mode mid-session would leave a running fixed-frequency timer stuck
/// at its previously computed interval until an unrelated settings change restarted it.
private func observeSystemPowerStateChanges() {
self.lowPowerModeObserver = NotificationCenter.default.addObserver(
forName: .NSProcessInfoPowerStateDidChange,
object: nil,
queue: .main)
{ [weak self] _ in
Task { @MainActor [weak self] in
guard let self, self.backgroundWorkLowPowerModePreference == .automatic else { return }
self.noteBackgroundWorkSettingsChanged()
}
}
}
}

Expand Down Expand Up @@ -555,8 +595,7 @@ extension SettingsStore {
if Self.isRunningTests, openAIWebBatterySaverDefault == nil {
userDefaults.set(false, forKey: "openAIWebBatterySaverEnabled")
}
let backgroundWorkLowPowerModeEnabled =
userDefaults.object(forKey: "backgroundWorkLowPowerModeEnabled") as? Bool ?? false
let backgroundWorkLowPowerModePreference = Self.loadLowPowerModePreference(userDefaults: userDefaults)
let providerStorageFootprintsDefault = userDefaults.object(forKey: "providerStorageFootprintsEnabled") as? Bool
let providerStorageFootprintsEnabled = providerStorageFootprintsDefault ?? false
if Self.isRunningTests, providerStorageFootprintsDefault == nil {
Expand Down Expand Up @@ -652,7 +691,7 @@ extension SettingsStore {
codexExternalOAuthSourcesAllowed: codexExternalOAuthSourcesAllowed,
openAIWebAccessEnabled: openAIWebAccessEnabled,
openAIWebBatterySaverEnabled: openAIWebBatterySaverEnabled,
backgroundWorkLowPowerModeEnabled: backgroundWorkLowPowerModeEnabled,
backgroundWorkLowPowerModePreference: backgroundWorkLowPowerModePreference,
providerStorageFootprintsEnabled: providerStorageFootprintsEnabled,
jetbrainsIDEBasePath: jetbrainsIDEBasePath,
mergeIcons: mergeIcons,
Expand Down Expand Up @@ -698,6 +737,20 @@ extension SettingsStore {
return frequency
}

private static func loadLowPowerModePreference(userDefaults: UserDefaults) -> LowPowerModePreference {
if let stored = userDefaults.string(forKey: "backgroundWorkLowPowerModePreference"),
let preference = LowPowerModePreference(rawValue: stored)
{
return preference
}

// Migrate the legacy on/off toggle, preserving prior behavior exactly (default off).
let legacyEnabled = userDefaults.object(forKey: "backgroundWorkLowPowerModeEnabled") as? Bool ?? false
let preference: LowPowerModePreference = legacyEnabled ? .on : .off
userDefaults.set(preference.rawValue, forKey: "backgroundWorkLowPowerModePreference")
return preference
}

private static func loadAdaptiveActivityScanConsent(
userDefaults: UserDefaults) -> AdaptiveActivityScanConsent
{
Expand Down
2 changes: 1 addition & 1 deletion Sources/CodexBar/SettingsStoreState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ struct SettingsDefaultsState {
var codexExternalOAuthSourcesAllowed: Bool
var openAIWebAccessEnabled: Bool
var openAIWebBatterySaverEnabled: Bool
var backgroundWorkLowPowerModeEnabled: Bool
var backgroundWorkLowPowerModePreference: LowPowerModePreference
var providerStorageFootprintsEnabled: Bool
var jetbrainsIDEBasePath: String
var mergeIcons: Bool
Expand Down
6 changes: 3 additions & 3 deletions Tests/CodexBarTests/AdaptiveRefreshHeuristicsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,16 @@ struct AdaptiveRefreshHeuristicsTests {
@Test
func `global low power mode clamps fixed and interactive adaptive heuristics`() {
let fixedStore = Self.makeStore(suite: "heuristics-low-power-fixed", frequency: .oneMinute)
fixedStore.settings.backgroundWorkLowPowerModeEnabled = true
fixedStore.settings.backgroundWorkLowPowerModePreference = .on
#expect(fixedStore.normalRefreshIntervalForHeuristics() == 1800.0)

let adaptiveStore = Self.makeStore(suite: "heuristics-low-power-adaptive", frequency: .adaptive)
adaptiveStore.settings.backgroundWorkLowPowerModeEnabled = true
adaptiveStore.settings.backgroundWorkLowPowerModePreference = .on
adaptiveStore.noteMenuOpened()
#expect(adaptiveStore.normalRefreshIntervalForHeuristics() == 1800.0)

let manualStore = Self.makeStore(suite: "heuristics-low-power-manual", frequency: .manual)
manualStore.settings.backgroundWorkLowPowerModeEnabled = true
manualStore.settings.backgroundWorkLowPowerModePreference = .on
#expect(manualStore.normalRefreshIntervalForHeuristics() == nil)
}

Expand Down
4 changes: 2 additions & 2 deletions Tests/CodexBarTests/AdaptiveRefreshTimerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ struct AdaptiveRefreshTimerTests {
let settings = Self.makeSettingsStore(
suite: "AdaptiveRefreshTimerTests-fixed-global-low-power",
frequency: .fiveMinutes)
settings.backgroundWorkLowPowerModeEnabled = true
settings.backgroundWorkLowPowerModePreference = .on
let store = Self.makeUsageStore(settings: settings, startupBehavior: .testing)

store.restartTimerWithSleepOverrideForTesting(.seconds(10))
Expand All @@ -230,7 +230,7 @@ struct AdaptiveRefreshTimerTests {
let settings = Self.makeSettingsStore(
suite: "AdaptiveRefreshTimerTests-adaptive-global-low-power",
frequency: .adaptive)
settings.backgroundWorkLowPowerModeEnabled = true
settings.backgroundWorkLowPowerModePreference = .on
let store = Self.makeUsageStore(settings: settings, startupBehavior: .testing)
let now = Date()
store.noteMenuOpened(at: now.addingTimeInterval(-10 * 60))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1090,7 +1090,7 @@ extension CodexBackgroundRefreshCoalescingTests {
suite: "CodexBackgroundRefreshCoalescingTests-forced-dashboard-battery")
settings.statusChecksEnabled = false
settings.costUsageEnabled = false
settings.backgroundWorkLowPowerModeEnabled = true
settings.backgroundWorkLowPowerModePreference = .on
let managedAccount = try Self.installManagedAccount(
email: "managed@example.com",
settings: settings)
Expand Down Expand Up @@ -1138,7 +1138,7 @@ extension CodexBackgroundRefreshCoalescingTests {
settings.statusChecksEnabled = false
settings.costUsageEnabled = false
settings.openAIWebBatterySaverEnabled = false
settings.backgroundWorkLowPowerModeEnabled = true
settings.backgroundWorkLowPowerModePreference = .on
let managedAccount = try Self.installManagedAccount(
email: "managed@example.com",
settings: settings)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2281,7 +2281,7 @@ struct ProviderArchitectureGatekeeperTests {
reason: "This exact app-runtime bridge coordinates provider-owned state through the shared controller."),
AllowedProviderConstruct(
path: "Sources/CodexBar/SettingsStore.swift",
line: 1073,
line: 1126,
anchor: "if !seen.contains(.factory), let zaiIndex = ordered.firstIndex(of: .zai) {",
expectedProviderIDs: ["factory", "minimax", "zai"],
expectedReferenceCount: 8,
Expand Down
2 changes: 1 addition & 1 deletion Tests/CodexBarTests/ProviderStorageFootprintTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,7 @@ struct ProviderStorageFootprintTests {
settings: settings,
environmentBase: ["CODEX_HOME": codexHome.path])
settings.providerStorageFootprintsEnabled = true
settings.backgroundWorkLowPowerModeEnabled = true
settings.backgroundWorkLowPowerModePreference = .on
store.managedCodexAccountsForStorageOverride = []

await store.refreshStorageFootprintsForOverviewNow()
Expand Down
20 changes: 17 additions & 3 deletions Tests/CodexBarTests/SettingsStoreCoverageTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -154,12 +154,13 @@ struct SettingsStoreCoverageTests {
let configStore = testConfigStore(suiteName: suite)

let initial = Self.makeSettingsStore(userDefaults: defaults, configStore: configStore)
#expect(initial.backgroundWorkLowPowerModePreference == .off)
#expect(initial.backgroundWorkLowPowerModeEnabled == false)
#expect(defaults.object(forKey: "backgroundWorkLowPowerModeEnabled") == nil)
#expect(defaults.string(forKey: "backgroundWorkLowPowerModePreference") == "off")
#expect(initial.effectiveOpenAIWebBatterySaverEnabled == false)

let revision = initial.backgroundWorkSettingsRevision
initial.backgroundWorkLowPowerModeEnabled = true
initial.backgroundWorkLowPowerModePreference = .on

#expect(initial.backgroundWorkSettingsRevision == revision + 1)
#expect(initial.effectiveOpenAIWebBatterySaverEnabled)
Expand All @@ -168,11 +169,24 @@ struct SettingsStoreCoverageTests {
#expect(reloaded.backgroundWorkLowPowerModeEnabled)
#expect(reloaded.effectiveOpenAIWebBatterySaverEnabled)

reloaded.backgroundWorkLowPowerModeEnabled = false
reloaded.backgroundWorkLowPowerModePreference = .off
reloaded.openAIWebBatterySaverEnabled = true
#expect(reloaded.effectiveOpenAIWebBatterySaverEnabled)
}

@Test
func `background low power mode migrates legacy enabled flag to on preference`() throws {
let suite = "SettingsStoreCoverageTests-background-low-power-migration"
let defaults = try #require(UserDefaults(suiteName: suite))
defaults.removePersistentDomain(forName: suite)
defaults.set(true, forKey: "backgroundWorkLowPowerModeEnabled")
let configStore = testConfigStore(suiteName: suite)

let migrated = Self.makeSettingsStore(userDefaults: defaults, configStore: configStore)
#expect(migrated.backgroundWorkLowPowerModePreference == .on)
#expect(defaults.string(forKey: "backgroundWorkLowPowerModePreference") == "on")
}

@Test
func `multi account menu layout persists and bridges legacy show all token accounts`() throws {
let suite = "SettingsStoreCoverageTests-multi-account-layout"
Expand Down