Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
5f90993
Reduce battery drain from OpenAI web extras
cbrane Mar 14, 2026
b6f4adc
Make OpenAI web refresh discoverable
cbrane Mar 15, 2026
fe9e18a
Merge remote-tracking branch 'origin/main' into fix-battery-draining-…
cbrane Mar 15, 2026
a563dba
Merge remote-tracking branch 'origin/main' into fix-battery-draining-…
cbrane Mar 16, 2026
f6ce9f6
Merge remote-tracking branch 'origin/main' into fix-battery-draining-…
cbrane Mar 18, 2026
8343a6f
Separate OpenAI web extras from battery saver
cbrane Mar 20, 2026
381498f
Respect battery saver on stale dashboard refresh
cbrane Mar 22, 2026
c53ad3e
Merge remote-tracking branch 'origin/main' into fix-battery-draining-…
cbrane Mar 23, 2026
b2e9900
Merge remote-tracking branch 'origin/main' into fix-battery-draining-…
cbrane Apr 1, 2026
ba8100d
Merge remote-tracking branch 'origin/main' into fix-battery-draining-…
cbrane Apr 6, 2026
a86ae90
Merge remote-tracking branch 'origin/main' into fix-battery-draining-…
cbrane Apr 8, 2026
91f6ca0
Merge remote-tracking branch 'origin/main' into codex/pr-529-audit-20…
ratulsarna Apr 9, 2026
f9d7717
Restore scoped Codex pane refresh
ratulsarna Apr 9, 2026
fca61d2
Merge branch 'main' into codex/pr-529-audit-20260409-152104
ratulsarna Apr 9, 2026
4b40edd
Merge branch 'main' into codex/pr-529-audit-20260409-152104
ratulsarna Apr 10, 2026
7b0dbfb
Fix Codex OpenAI web refresh defaults
ratulsarna Apr 10, 2026
e0b666c
Preserve legacy Codex web access inference
ratulsarna Apr 10, 2026
69be2b0
Skip WebKit cache tests on CI
ratulsarna Apr 10, 2026
22c0de5
Fix Codex dashboard refresh test defaults
ratulsarna Apr 10, 2026
21e9ea6
Fix managed OpenAI web test defaults
ratulsarna Apr 10, 2026
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
1 change: 1 addition & 0 deletions Sources/CodexBar/MenuDescriptor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,7 @@ struct MenuDescriptor {
entries.append(.action("Update ready, restart now?", .installUpdate))
}
entries.append(contentsOf: [
.action("Refresh", .refresh),
.action("Settings...", .settings),
.action("About CodexBar", .about),
.action("Quit", .quit),
Expand Down
22 changes: 13 additions & 9 deletions Sources/CodexBar/PreferencesProvidersPane.swift
Original file line number Diff line number Diff line change
Expand Up @@ -66,15 +66,7 @@ struct ProvidersPane: View {
isErrorExpanded: self.expandedBinding(for: provider),
onCopyError: { text in self.copyToPasteboard(text) },
onRefresh: {
Task { @MainActor in
await ProviderInteractionContext.$current.withValue(.userInitiated) {
if provider == .codex {
await self.store.refreshCodexAccountScopedState(allowDisabled: true)
} else {
await self.store.refreshProvider(provider, allowDisabled: true)
}
}
}
self.triggerRefresh(for: provider)
},
showsSupplementarySettingsContent: self.codexAccountsSectionState(for: provider) != nil,
supplementarySettingsContent: {
Expand Down Expand Up @@ -156,6 +148,18 @@ struct ProvidersPane: View {
self.selectedProvider = self.providers.first
}

private func triggerRefresh(for provider: UsageProvider) {
Task { @MainActor in
await ProviderInteractionContext.$current.withValue(.userInitiated) {
if provider == .codex {
await self.store.refreshCodexAccountScopedState(allowDisabled: true)
} else {
await self.store.refreshProvider(provider, allowDisabled: true)
}
}
}
}

func binding(for provider: UsageProvider) -> Binding<Bool> {
let meta = self.store.metadata(for: provider)
return Binding(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ struct CodexProviderImplementation: ProviderImplementation {
for: .codex)
}
})
let batterySaverBinding = context.boolBinding(\.openAIWebBatterySaverEnabled)

return [
ProviderSettingsToggleDescriptor(
Expand All @@ -85,14 +86,32 @@ struct CodexProviderImplementation: ProviderImplementation {
ProviderSettingsToggleDescriptor(
id: "codex-openai-web-extras",
title: "OpenAI web extras",
subtitle: "Show usage breakdown, credits history, and code review via chatgpt.com.",
subtitle: [
"Optional.",
"Turn this on to show code review, usage breakdown, and credits history via chatgpt.com.",
].joined(separator: " "),
binding: extrasBinding,
statusText: nil,
actions: [],
isVisible: nil,
onChange: nil,
onAppDidBecomeActive: nil,
onAppearWhenEnabled: nil),
ProviderSettingsToggleDescriptor(
id: "codex-openai-web-battery-saver",
title: "Battery Saver",
subtitle: [
"Recommended.",
"Limits background chatgpt.com refreshes to reduce battery and network usage.",
"Dashboard extras may stay stale until you refresh them manually.",
].joined(separator: " "),
binding: batterySaverBinding,
statusText: nil,
actions: [],
isVisible: { context.settings.openAIWebAccessEnabled },
onChange: nil,
onAppDidBecomeActive: nil,
onAppearWhenEnabled: nil),
]
}

Expand Down
11 changes: 11 additions & 0 deletions Sources/CodexBar/SettingsStore+Defaults.swift
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,17 @@ extension SettingsStore {
}
}

var openAIWebBatterySaverEnabled: Bool {
get { self.defaultsState.openAIWebBatterySaverEnabled }
set {
self.defaultsState.openAIWebBatterySaverEnabled = newValue
self.userDefaults.set(newValue, forKey: "openAIWebBatterySaverEnabled")
CodexBarLog.logger(LogCategories.settings).info(
"OpenAI web battery saver updated",
metadata: ["enabled": newValue ? "1" : "0"])
}
}

var jetbrainsIDEBasePath: String {
get { self.defaultsState.jetbrainsIDEBasePath }
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 @@ -27,6 +27,7 @@ extension SettingsStore {
_ = self.claudeWebExtrasEnabled
_ = self.showOptionalCreditsAndExtraUsage
_ = self.openAIWebAccessEnabled
_ = self.openAIWebBatterySaverEnabled
_ = self.codexUsageDataSource
_ = self.codexActiveSource
_ = self.claudeUsageDataSource
Expand Down
28 changes: 25 additions & 3 deletions Sources/CodexBar/SettingsStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,8 @@ final class SettingsStore {
copilotTokenStore: any CopilotTokenStoring = KeychainCopilotTokenStore(),
tokenAccountStore: any ProviderTokenAccountStoring = FileTokenAccountStore())
{
let hasStoredOpenAIWebAccessPreference = userDefaults.object(forKey: "openAIWebAccessEnabled") != nil
let hadExistingConfig = (try? configStore.load()) != nil
let legacyStores = CodexBarConfigMigrator.LegacyStores(
zaiTokenStore: zaiTokenStore,
syntheticTokenStore: syntheticTokenStore,
Expand Down Expand Up @@ -159,7 +161,13 @@ final class SettingsStore {
self.ensureAlibabaProviderAutoEnabledIfNeeded()
self.applyTokenCostDefaultIfNeeded()
if self.claudeUsageDataSource != .cli { self.claudeWebExtrasEnabled = false }
self.openAIWebAccessEnabled = self.codexCookieSource.isEnabled
if hasStoredOpenAIWebAccessPreference {
self.openAIWebAccessEnabled = self.defaultsState.openAIWebAccessEnabled
} else {
self.openAIWebAccessEnabled = Self.inferredInitialOpenAIWebAccessEnabled(
config: config,
hadExistingConfig: hadExistingConfig)
}
if Self.shouldBridgeSharedDefaults(for: userDefaults) {
Self.sharedDefaults?.set(self.debugDisableKeychainAccess, forKey: "debugDisableKeychainAccess")
}
Expand All @@ -168,6 +176,16 @@ final class SettingsStore {
}

extension SettingsStore {
private static func inferredInitialOpenAIWebAccessEnabled(
config: CodexBarConfig,
hadExistingConfig: Bool) -> Bool
{
guard let codex = config.providerConfig(for: .codex) else { return false }
if let cookieSource = codex.cookieSource { return cookieSource.isEnabled }
if codex.sanitizedCookieHeader != nil { return true }
return hadExistingConfig
}

private static func loadDefaultsState(userDefaults: UserDefaults) -> SettingsDefaultsState {
let refreshDefault = userDefaults.string(forKey: "refreshFrequency")
.flatMap(RefreshFrequency.init(rawValue:))
Expand Down Expand Up @@ -230,8 +248,11 @@ extension SettingsStore {
let showOptionalCreditsAndExtraUsage = creditsExtrasDefault ?? true
if creditsExtrasDefault == nil { userDefaults.set(true, forKey: "showOptionalCreditsAndExtraUsage") }
let openAIWebAccessDefault = userDefaults.object(forKey: "openAIWebAccessEnabled") as? Bool
let openAIWebAccessEnabled = openAIWebAccessDefault ?? true
if openAIWebAccessDefault == nil { userDefaults.set(true, forKey: "openAIWebAccessEnabled") }
let openAIWebAccessEnabled = openAIWebAccessDefault ?? false
if openAIWebAccessDefault == nil { userDefaults.set(false, forKey: "openAIWebAccessEnabled") }
let openAIWebBatterySaverDefault = userDefaults.object(forKey: "openAIWebBatterySaverEnabled") as? Bool
let openAIWebBatterySaverEnabled = openAIWebBatterySaverDefault ?? false
if openAIWebBatterySaverDefault == nil { userDefaults.set(false, forKey: "openAIWebBatterySaverEnabled") }
let jetbrainsIDEBasePath = userDefaults.string(forKey: "jetbrainsIDEBasePath") ?? ""
let mergeIcons = userDefaults.object(forKey: "mergeIcons") as? Bool ?? true
let switcherShowsIcons = userDefaults.object(forKey: "switcherShowsIcons") as? Bool ?? true
Expand Down Expand Up @@ -269,6 +290,7 @@ extension SettingsStore {
claudeWebExtrasEnabledRaw: claudeWebExtrasEnabledRaw,
showOptionalCreditsAndExtraUsage: showOptionalCreditsAndExtraUsage,
openAIWebAccessEnabled: openAIWebAccessEnabled,
openAIWebBatterySaverEnabled: openAIWebBatterySaverEnabled,
jetbrainsIDEBasePath: jetbrainsIDEBasePath,
mergeIcons: mergeIcons,
switcherShowsIcons: switcherShowsIcons,
Expand Down
1 change: 1 addition & 0 deletions Sources/CodexBar/SettingsStoreState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ struct SettingsDefaultsState {
var claudeWebExtrasEnabledRaw: Bool
var showOptionalCreditsAndExtraUsage: Bool
var openAIWebAccessEnabled: Bool
var openAIWebBatterySaverEnabled: Bool
var jetbrainsIDEBasePath: String
var mergeIcons: Bool
var switcherShowsIcons: Bool
Expand Down
24 changes: 24 additions & 0 deletions Sources/CodexBar/UsageStore+BackgroundRefresh.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import CodexBarCore
import Foundation

@MainActor
extension UsageStore {
func clearDisabledProviderState(enabledProviders: Set<UsageProvider>) {
for provider in UsageProvider.allCases where !enabledProviders.contains(provider) {
self.refreshingProviders.remove(provider)
self.snapshots.removeValue(forKey: provider)
self.errors[provider] = nil
self.lastSourceLabels.removeValue(forKey: provider)
self.lastFetchAttempts.removeValue(forKey: provider)
self.accountSnapshots.removeValue(forKey: provider)
self.tokenSnapshots.removeValue(forKey: provider)
self.tokenErrors[provider] = nil
self.failureGates[provider]?.reset()
self.tokenFailureGates[provider]?.reset()
self.statuses.removeValue(forKey: provider)
self.lastKnownSessionRemaining.removeValue(forKey: provider)
self.lastKnownSessionWindowSource.removeValue(forKey: provider)
self.lastTokenFetchAt.removeValue(forKey: provider)
}
}
}
1 change: 1 addition & 0 deletions Sources/CodexBar/UsageStore+Logging.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ extension UsageStore {
"ampCookieSource": self.settings.ampCookieSource.rawValue,
"ollamaCookieSource": self.settings.ollamaCookieSource.rawValue,
"openAIWebAccess": self.settings.openAIWebAccessEnabled ? "1" : "0",
"openAIWebBatterySaver": self.settings.openAIWebBatterySaverEnabled ? "1" : "0",
"claudeWebExtras": self.settings.claudeWebExtrasEnabled ? "1" : "0",
"kiloExtras": self.settings.kiloExtrasEnabled ? "1" : "0",
]
Expand Down
103 changes: 91 additions & 12 deletions Sources/CodexBar/UsageStore+OpenAIWeb.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,22 @@
import CodexBarCore
import Foundation

struct OpenAIWebRefreshGateContext {
let force: Bool
let accountDidChange: Bool
let lastError: String?
let lastSnapshotAt: Date?
let lastAttemptAt: Date?
let now: Date
let refreshInterval: TimeInterval
}

struct OpenAIWebRefreshPolicyContext {
let accessEnabled: Bool
let batterySaverEnabled: Bool
let force: Bool
}

// MARK: - OpenAI web lifecycle

extension UsageStore {
Expand Down Expand Up @@ -31,15 +47,28 @@ extension UsageStore {
}

func requestOpenAIDashboardRefreshIfStale(reason: String) {
guard self.isEnabled(.codex), self.settings.codexCookieSource.isEnabled else { return }
guard self.isEnabled(.codex),
self.settings.openAIWebAccessEnabled,
self.settings.codexCookieSource.isEnabled
else { return }
let now = Date()
let refreshInterval = self.openAIWebRefreshIntervalSeconds()
let lastUpdatedAt = self.openAIDashboard?.updatedAt ?? self.lastOpenAIDashboardSnapshot?.updatedAt
if let lastUpdatedAt, now.timeIntervalSince(lastUpdatedAt) < refreshInterval { return }
let stamp = now.formatted(date: .abbreviated, time: .shortened)
self.logOpenAIWeb("[\(stamp)] OpenAI web refresh request: \(reason)")
let forceRefresh = Self.forceOpenAIWebRefreshForStaleRequest(
batterySaverEnabled: self.settings.openAIWebBatterySaverEnabled)
self.openAIWebLogger.debug(
"OpenAI web stale refresh gate",
metadata: [
"reason": reason,
"force": forceRefresh ? "1" : "0",
"batterySaverEnabled": self.settings.openAIWebBatterySaverEnabled ? "1" : "0",
"interaction": ProviderInteractionContext.current == .userInitiated ? "user" : "background",
])
let expectedGuard = self.currentCodexOpenAIWebRefreshGuard()
Task { await self.refreshOpenAIDashboardIfNeeded(force: true, expectedGuard: expectedGuard) }
Task { await self.refreshOpenAIDashboardIfNeeded(force: forceRefresh, expectedGuard: expectedGuard) }
}

func applyOpenAIDashboard(
Expand Down Expand Up @@ -95,6 +124,7 @@ extension UsageStore {
return
}

OpenAIDashboardFetcher.evictAllCachedWebViews()
await MainActor.run {
if let cached = self.lastOpenAIDashboardSnapshot {
self.openAIDashboard = cached
Expand Down Expand Up @@ -132,6 +162,7 @@ extension UsageStore {
return
}

OpenAIDashboardFetcher.evictAllCachedWebViews()
await MainActor.run {
self.lastOpenAIDashboardError = [
"OpenAI web access requires a signed-in chatgpt.com session.",
Expand Down Expand Up @@ -311,10 +342,11 @@ extension UsageStore {
bypassCoalescing: Bool = false,
allowCodexUsageBackfill: Bool = true) async
{
guard self.isEnabled(.codex), self.settings.codexCookieSource.isEnabled else {
self.resetOpenAIWebState()
return
}
self.syncOpenAIWebState()
guard self.isEnabled(.codex),
self.settings.openAIWebAccessEnabled,
self.settings.codexCookieSource.isEnabled
else { return }
if self.openAIWebManagedTargetStoreIsUnreadable() {
await self.failClosedRefreshForUnreadableManagedCodexStore()
return
Expand All @@ -341,14 +373,18 @@ extension UsageStore {

let now = Date()
let minInterval = self.openAIWebRefreshIntervalSeconds()
if !force,
!self.openAIWebAccountDidChange,
self.lastOpenAIDashboardError == nil,
let snapshot = self.lastOpenAIDashboardSnapshot,
now.timeIntervalSince(snapshot.updatedAt) < minInterval
{
let refreshGate = OpenAIWebRefreshGateContext(
force: force,
accountDidChange: self.openAIWebAccountDidChange,
lastError: self.lastOpenAIDashboardError,
lastSnapshotAt: self.lastOpenAIDashboardSnapshot?.updatedAt,
lastAttemptAt: self.lastOpenAIDashboardAttemptAt,
now: now,
refreshInterval: minInterval)
if Self.shouldSkipOpenAIWebRefresh(refreshGate) {
return
}
self.lastOpenAIDashboardAttemptAt = now

let taskToken = UUID()
let context = OpenAIDashboardRefreshContext(
Expand Down Expand Up @@ -610,6 +646,7 @@ extension UsageStore {
self.lastOpenAIDashboardSnapshot = nil
self.lastOpenAIDashboardAttachmentAuthorized = false
self.lastOpenAIDashboardError = nil
self.lastOpenAIDashboardAttemptAt = nil
self.openAIDashboardRequiresLogin = true
self.openAIDashboardCookieImportStatus = "Codex account changed; importing browser cookies…"
self.lastOpenAIDashboardCookieImportAttemptAt = nil
Expand Down Expand Up @@ -994,12 +1031,14 @@ extension UsageStore {

func resetOpenAIWebState() {
self.invalidateOpenAIDashboardRefreshTask()
OpenAIDashboardFetcher.evictAllCachedWebViews()
self.openAIDashboard = nil
self.openAIDashboardAttachmentAuthorized = false
self.lastOpenAIDashboardError = nil
self.lastOpenAIDashboardSnapshot = nil
self.lastOpenAIDashboardAttachmentAuthorized = false
self.lastOpenAIDashboardTargetEmail = nil
self.lastOpenAIDashboardAttemptAt = nil
self.openAIDashboardRequiresLogin = false
self.openAIDashboardCookieImportStatus = nil
self.openAIDashboardCookieImportDebugLog = nil
Expand Down Expand Up @@ -1079,6 +1118,46 @@ extension UsageStore {
// MARK: - OpenAI web error messaging

extension UsageStore {
nonisolated static func shouldRunOpenAIWebRefresh(_ context: OpenAIWebRefreshPolicyContext) -> Bool {
guard context.accessEnabled else { return false }
return context.force || !context.batterySaverEnabled
}

nonisolated static func forceOpenAIWebRefreshForStaleRequest(batterySaverEnabled: Bool) -> Bool {
!batterySaverEnabled
}

nonisolated static func shouldSkipOpenAIWebRefresh(_ context: OpenAIWebRefreshGateContext) -> Bool {
if context.force || context.accountDidChange { return false }
if let lastAttemptAt = context.lastAttemptAt,
context.now.timeIntervalSince(lastAttemptAt) < context.refreshInterval
{
return true
}
if context.lastError == nil,
let lastSnapshotAt = context.lastSnapshotAt,
context.now.timeIntervalSince(lastSnapshotAt) < context.refreshInterval
{
return true
}
return false
}

func syncOpenAIWebState() {
guard self.isEnabled(.codex),
self.settings.openAIWebAccessEnabled,
self.settings.codexCookieSource.isEnabled
else {
self.resetOpenAIWebState()
return
}

let targetEmail = self.currentCodexOpenAIWebTargetEmail(
allowCurrentSnapshotFallback: true,
allowLastKnownLiveFallback: true)
self.handleOpenAIWebTargetEmailChangeIfNeeded(targetEmail: targetEmail)
}

func openAIDashboardFriendlyError(
body: String,
targetEmail: String?,
Expand Down
Loading