Skip to content

Commit 8964c65

Browse files
committed
refactor: model menu-open refresh policy
1 parent 79c24a4 commit 8964c65

5 files changed

Lines changed: 116 additions & 17 deletions

File tree

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import CodexBarCore
2+
3+
struct MenuOpenRefreshPlan: Equatable {
4+
struct Inputs {
5+
let refreshAllOnOpen: Bool
6+
let enabledProviders: [UsageProvider]
7+
let visibleProviders: [UsageProvider]
8+
let refreshingProviders: Set<UsageProvider>
9+
let staleProviders: Set<UsageProvider>
10+
let missingProviders: Set<UsageProvider>
11+
}
12+
13+
enum Scheduling: Equatable {
14+
case sequential
15+
case concurrent
16+
}
17+
18+
let providers: [UsageProvider]
19+
let scheduling: Scheduling
20+
21+
static func resolve(_ inputs: Inputs) -> Self {
22+
if inputs.refreshAllOnOpen {
23+
return Self(providers: inputs.enabledProviders, scheduling: .concurrent)
24+
}
25+
26+
let enabled = Set(inputs.enabledProviders)
27+
let providers = inputs.visibleProviders.filter {
28+
enabled.contains($0) &&
29+
(inputs.refreshingProviders.contains($0) || inputs.staleProviders.contains($0) ||
30+
inputs.missingProviders.contains($0))
31+
}
32+
return Self(providers: providers, scheduling: .sequential)
33+
}
34+
}

Sources/CodexBar/SettingsStore+Defaults.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ extension SettingsStore {
1313
}
1414
}
1515

16-
/// When enabled, opening the menu refreshes usage for every enabled provider (on top of the
17-
/// periodic refresh clock, which is left untouched). See `scheduleOpenMenuRefresh`.
16+
/// When enabled, keeping the menu open through its short refresh delay fetches usage for every
17+
/// enabled provider. The periodic refresh clock remains unchanged. See `scheduleOpenMenuRefresh`.
1818
var refreshAllProvidersOnMenuOpen: Bool {
1919
get { self.defaultsState.refreshAllProvidersOnMenuOpen }
2020
set {

Sources/CodexBar/StatusItemController+Menu.swift

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1133,20 +1133,16 @@ extension StatusItemController {
11331133
#endif
11341134
guard self.openMenus[ObjectIdentifier(menu)] != nil else { return }
11351135
let refreshAllOnOpen = self.settings.refreshAllProvidersOnMenuOpen
1136-
let availableProviders = Set(self.store.enabledProvidersForBackgroundWork())
1137-
// When "refresh all providers on open" is enabled, consider every enabled provider and refresh
1138-
// it regardless of freshness; otherwise keep the conservative stale/missing retry set scoped to
1139-
// the providers actually rendered in this menu.
1140-
let candidateProviders = refreshAllOnOpen
1141-
? self.store.enabledProvidersForBackgroundWork()
1142-
: self.delayedRefreshRetryProviders(for: menu)
1143-
let retryProviders = candidateProviders.filter {
1144-
availableProviders.contains($0) &&
1145-
(refreshAllOnOpen ||
1146-
self.store.refreshingProviders.contains($0) ||
1147-
self.store.isStale(provider: $0) ||
1148-
self.store.snapshot(for: $0) == nil)
1149-
}
1136+
let enabledProviders = self.store.enabledProvidersForBackgroundWork()
1137+
let visibleProviders = self.delayedRefreshRetryProviders(for: menu)
1138+
let plan = MenuOpenRefreshPlan.resolve(.init(
1139+
refreshAllOnOpen: refreshAllOnOpen,
1140+
enabledProviders: enabledProviders,
1141+
visibleProviders: visibleProviders,
1142+
refreshingProviders: self.store.refreshingProviders,
1143+
staleProviders: Set(visibleProviders.filter { self.store.isStale(provider: $0) }),
1144+
missingProviders: Set(visibleProviders.filter { self.store.snapshot(for: $0) == nil })))
1145+
let retryProviders = plan.providers
11501146
guard !retryProviders.isEmpty else {
11511147
self.clearSatisfiedDeferredMenuInteractionRefreshes(
11521148
for: self.delayedRefreshRetryProviders(for: menu))
@@ -1162,7 +1158,7 @@ extension StatusItemController {
11621158
}
11631159
self.deferMenuInteractionRefreshIfNeeded(providers: retryProviders)
11641160
await ProviderInteractionContext.$current.withValue(.background) {
1165-
if refreshAllOnOpen {
1161+
if plan.scheduling == .concurrent {
11661162
// Refresh concurrently so one slow provider doesn't delay the rest, mirroring the
11671163
// periodic refresh in `UsageStore.runRefresh`. `coalesceIfRefreshing` makes each call
11681164
// wait for any in-flight refresh (e.g. a manual refresh) instead of overriding it.
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import CodexBarCore
2+
import Testing
3+
@testable import CodexBar
4+
5+
struct MenuOpenRefreshPlanTests {
6+
@Test
7+
func `refresh all selects every enabled provider concurrently`() {
8+
let plan = MenuOpenRefreshPlan.resolve(.init(
9+
refreshAllOnOpen: true,
10+
enabledProviders: [.codex, .claude, .factory],
11+
visibleProviders: [.codex],
12+
refreshingProviders: [],
13+
staleProviders: [],
14+
missingProviders: []))
15+
16+
#expect(plan.providers == [.codex, .claude, .factory])
17+
#expect(plan.scheduling == .concurrent)
18+
}
19+
20+
@Test
21+
func `ordinary refresh selects only visible enabled retries sequentially`() {
22+
let plan = MenuOpenRefreshPlan.resolve(.init(
23+
refreshAllOnOpen: false,
24+
enabledProviders: [.codex, .claude, .factory],
25+
visibleProviders: [.factory, .codex, .claude, .cursor],
26+
refreshingProviders: [.factory],
27+
staleProviders: [.codex],
28+
missingProviders: [.claude, .cursor]))
29+
30+
#expect(plan.providers == [.factory, .codex, .claude])
31+
#expect(plan.scheduling == .sequential)
32+
}
33+
34+
@Test
35+
func `ordinary refresh skips fresh providers`() {
36+
let plan = MenuOpenRefreshPlan.resolve(.init(
37+
refreshAllOnOpen: false,
38+
enabledProviders: [.codex],
39+
visibleProviders: [.codex],
40+
refreshingProviders: [],
41+
staleProviders: [],
42+
missingProviders: []))
43+
44+
#expect(plan.providers.isEmpty)
45+
}
46+
}

Tests/CodexBarTests/SettingsStoreTests.swift

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,29 @@ struct SettingsStoreTests {
8686
#expect(storeB.refreshFrequency.seconds == 900)
8787
}
8888

89+
@Test
90+
func `refresh on open defaults off and persists`() throws {
91+
let suite = "SettingsStoreTests-refresh-on-open"
92+
let defaults = try #require(UserDefaults(suiteName: suite))
93+
defaults.removePersistentDomain(forName: suite)
94+
let configStore = testConfigStore(suiteName: suite)
95+
let store = SettingsStore(
96+
userDefaults: defaults,
97+
configStore: configStore,
98+
zaiTokenStore: NoopZaiTokenStore(),
99+
syntheticTokenStore: NoopSyntheticTokenStore())
100+
101+
#expect(store.refreshAllProvidersOnMenuOpen == false)
102+
store.refreshAllProvidersOnMenuOpen = true
103+
104+
let reloaded = SettingsStore(
105+
userDefaults: defaults,
106+
configStore: configStore,
107+
zaiTokenStore: NoopZaiTokenStore(),
108+
syntheticTokenStore: NoopSyntheticTokenStore())
109+
#expect(reloaded.refreshAllProvidersOnMenuOpen == true)
110+
}
111+
89112
@Test
90113
func `weekly confetti setting defaults off and persists`() throws {
91114
let suite = "SettingsStoreTests-weekly-confetti"

0 commit comments

Comments
 (0)