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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## 0.49.1 — Unreleased

### Fixed
- Codex: publish refreshed core quota immediately while optional Credits and OpenAI Web enrichment continues, without unfreezing cards whose layout still needs reconciliation (#2799). Thanks @Yuxin-Qiao!
- Menu bar: keep DeepSeek balances compact and consistent between saved custom layouts and their live editor preview (#2638). Thanks @Yuxin-Qiao!
- Settings: show CodexBar in the Dock while Settings or an update dialog is open, so Check for Updates and new-version prompts reliably appear in front.
- Claude CLI: let explicit CLI usage and Auto fallback delegate authentication to the installed Claude executable, so an unavailable browser session no longer masks usable reduced-fidelity CLI usage.
Expand Down
40 changes: 35 additions & 5 deletions Sources/CodexBar/MenuCardRefreshMonitor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ final class MenuCardRefreshMonitor {
/// refreshing one provider does not stall or unfreeze another.
private var manualRefreshProviders: Set<ProviderInstanceID> = []
private var frozenManualRefreshModels: [ProviderInstanceID: UsageMenuCardView.Model] = [:]
/// Core models published before optional enrichment finishes. These keep an already-hosted card on the
/// refreshed quota if a later enrichment step temporarily changes the model's tracked layout.
private var publishedManualRefreshModels: [ProviderInstanceID: UsageMenuCardView.Model] = [:]

/// True while any manual refresh (global or per-provider) is running.
var isManualRefreshInFlight: Bool {
Expand Down Expand Up @@ -55,16 +58,38 @@ final class MenuCardRefreshMonitor {
if let provider {
self.manualRefreshProviders.remove(provider.instanceID)
self.frozenManualRefreshModels[provider.instanceID] = nil
self.publishedManualRefreshModels[provider.instanceID] = nil
} else {
self.globalManualRefreshInFlight = false
self.frozenManualRefreshModels.removeAll(keepingCapacity: true)
self.publishedManualRefreshModels.removeAll(keepingCapacity: true)
}
}

/// Ends a provider-scoped refresh only when the hosted card can adopt the resolved model without a rebuild.
/// The published model stays pinned as a compatible fallback until the caller reaches its final reconciliation.
@discardableResult
func publishResolvedModelIfCompatible(for provider: UsageProvider) -> Bool {
let instanceID = provider.instanceID
guard self.manualRefreshProviders.contains(instanceID),
let frozen = self.frozenManualRefreshModels[instanceID],
let resolved = self.resolveModel(provider),
frozen.hasCompatibleTrackedLayout(with: resolved)
else {
return false
}

self.manualRefreshProviders.remove(instanceID)
self.frozenManualRefreshModels[instanceID] = nil
self.publishedManualRefreshModels[instanceID] = resolved
return true
}

func resetManualRefresh() {
self.globalManualRefreshInFlight = false
self.manualRefreshProviders.removeAll(keepingCapacity: true)
self.frozenManualRefreshModels.removeAll(keepingCapacity: true)
self.publishedManualRefreshModels.removeAll(keepingCapacity: true)
}

func isManualRefreshInFlight(for provider: UsageProvider) -> Bool {
Expand All @@ -91,12 +116,17 @@ final class MenuCardRefreshMonitor {
return fallback
}

guard let resolved = self.resolveModel(provider),
fallback.hasCompatibleTrackedLayout(with: resolved)
else {
return fallback
if let resolved = self.resolveModel(provider),
fallback.hasCompatibleTrackedLayout(with: resolved)
{
return resolved
}
if let published = self.publishedManualRefreshModels[provider.instanceID],
fallback.hasCompatibleTrackedLayout(with: published)
{
return published
}
return resolved
return fallback
}

func subtitle(
Expand Down
6 changes: 6 additions & 0 deletions Sources/CodexBar/StatusItemController+Actions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,12 @@ extension StatusItemController: StatusItemMenuPersistentActionDelegate {
let refreshStartedAt = Date()
await self.store.refreshProvider(provider)
guard !Task.isCancelled, !self.hasPreparedForAppShutdown else { return }
// Provider-specific by design: Codex publishes a compatible core quota model before its optional
// credits and OpenAI Web enrichment stages below. Incompatible cards remain frozen until the final
// menu reconciliation, so they never lose their loading state while still showing the old layout.
if provider == .codex {
self.menuCardRefreshMonitor.publishResolvedModelIfCompatible(for: provider)
}
await self.store.refreshProviderStatus(provider)
guard !Task.isCancelled, !self.hasPreparedForAppShutdown else { return }
await self.store.refreshTokenUsageNow(for: provider, force: true)
Expand Down
12 changes: 6 additions & 6 deletions Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2363,47 +2363,47 @@ struct ProviderArchitectureGatekeeperTests {
reason: "This exact app-runtime bridge coordinates provider-owned state through the shared controller."),
AllowedProviderConstruct(
path: "Sources/CodexBar/StatusItemController+Actions.swift",
line: 371,
line: 377,
anchor: "if provider == .qoder {",
expectedProviderIDs: ["claude", "qoder"],
expectedReferenceCount: 3,
expectedReferenceFingerprint: ["qoder@0", "qoder@3", "claude@7"],
reason: "This exact app-runtime bridge coordinates provider-owned state through the shared controller."),
AllowedProviderConstruct(
path: "Sources/CodexBar/StatusItemController+Actions.swift",
line: 441,
line: 447,
anchor: "?? (self.store.isEnabled(.codex) ? .codex : self.store.enabledFirstPartyProviders().first)",
expectedProviderIDs: ["codex"],
expectedReferenceCount: 4,
expectedReferenceFingerprint: ["codex@0", "codex@0", "codex@2", "codex@8"],
reason: "This exact app-runtime bridge coordinates provider-owned state through the shared controller."),
AllowedProviderConstruct(
path: "Sources/CodexBar/StatusItemController+Actions.swift",
line: 462,
line: 468,
anchor: "?? (self.store.isEnabled(.codex) ? .codex : self.store.enabledFirstPartyProviders().first)",
expectedProviderIDs: ["claude", "codex"],
expectedReferenceCount: 4,
expectedReferenceFingerprint: ["codex@0", "codex@0", "codex@2", "claude@10"],
reason: "This exact app-runtime bridge coordinates provider-owned state through the shared controller."),
AllowedProviderConstruct(
path: "Sources/CodexBar/StatusItemController+Actions.swift",
line: 535,
line: 541,
anchor: "?? .codex",
expectedProviderIDs: ["codex"],
expectedReferenceCount: 1,
expectedReferenceFingerprint: ["codex@0"],
reason: "This exact app-runtime bridge coordinates provider-owned state through the shared controller."),
AllowedProviderConstruct(
path: "Sources/CodexBar/StatusItemController+Actions.swift",
line: 594,
line: 600,
anchor: "self.lazyStatusItem(for: provider ?? .codex)",
expectedProviderIDs: ["codex"],
expectedReferenceCount: 1,
expectedReferenceFingerprint: ["codex@0"],
reason: "This exact app-runtime bridge coordinates provider-owned state through the shared controller."),
AllowedProviderConstruct(
path: "Sources/CodexBar/StatusItemController+Actions.swift",
line: 698,
line: 704,
anchor: "return .codex",
expectedProviderIDs: ["codex"],
expectedReferenceCount: 1,
Expand Down
143 changes: 143 additions & 0 deletions Tests/CodexBarTests/StatusMenuPersistentRefreshTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,149 @@ struct StatusMenuPersistentRefreshTests {
#expect(refreshed.metrics.map(\.percent) != fallback.metrics.map(\.percent))
}

@Test
func `refresh monitor publishes compatible core and pins it until final reconciliation`() throws {
let settings = self.makeSettings()
self.enableOnly([.codex], settings: settings)
let controller = self.makeController(settings: settings)
defer { controller.releaseStatusItemsForTesting() }
let monitor = controller.menuCardRefreshMonitor
let now = Date(timeIntervalSince1970: 1_700_000_000)
controller.store.snapshots[.codex] = UsageSnapshot(
primary: RateWindow(
usedPercent: 10,
windowMinutes: 300,
resetsAt: now.addingTimeInterval(3600),
resetDescription: nil),
secondary: nil,
updatedAt: now)
let frozen = try #require(controller.menuCardModel(for: .codex))
monitor.beginManualRefresh(frozenModels: [.codex: frozen], provider: .codex)
defer { monitor.endManualRefresh(for: .codex) }

controller.store.snapshots[.codex] = UsageSnapshot(
primary: RateWindow(
usedPercent: 40,
windowMinutes: 300,
resetsAt: now.addingTimeInterval(3600),
resetDescription: nil),
secondary: nil,
updatedAt: now.addingTimeInterval(1))
let core = try #require(controller.menuCardModel(for: .codex))

#expect(monitor.publishResolvedModelIfCompatible(for: .codex))
#expect(!monitor.isManualRefreshInFlight(for: .codex))
#expect(monitor.model(for: .codex, fallback: frozen).metrics.map(\.percent) == core.metrics.map(\.percent))

controller.store.snapshots[.codex] = UsageSnapshot(
primary: RateWindow(
usedPercent: 40,
windowMinutes: 300,
resetsAt: now.addingTimeInterval(3600),
resetDescription: nil),
secondary: RateWindow(
usedPercent: 70,
windowMinutes: 10080,
resetsAt: now.addingTimeInterval(7200),
resetDescription: nil),
updatedAt: now.addingTimeInterval(2))
let enriched = try #require(controller.menuCardModel(for: .codex))
let visibleBeforeReconciliation = monitor.model(for: .codex, fallback: frozen)
let visibleAfterReconciliation = monitor.model(for: .codex, fallback: enriched)

#expect(visibleBeforeReconciliation.metrics.map(\.percent) == core.metrics.map(\.percent))
#expect(visibleAfterReconciliation.metrics.map(\.percent) == enriched.metrics.map(\.percent))
if ProcessInfo.processInfo.environment["CODEXBAR_REFRESH_PROBE"] == "1" {
print(
"CODEXBAR_REFRESH_PROBE compatible-layout-shift refreshing=false pinned=" +
"\(visibleBeforeReconciliation.metrics.first?.percentLabel ?? "none") " +
"reconciledRows=\(visibleAfterReconciliation.metrics.count)")
}
}

@Test
func `refresh monitor keeps incompatible core frozen until reconciliation`() throws {
let settings = self.makeSettings()
self.enableOnly([.codex], settings: settings)
let controller = self.makeController(settings: settings)
defer { controller.releaseStatusItemsForTesting() }
let monitor = controller.menuCardRefreshMonitor
let now = Date(timeIntervalSince1970: 1_700_000_000)
controller.store.snapshots[.codex] = UsageSnapshot(
primary: RateWindow(
usedPercent: 15,
windowMinutes: 300,
resetsAt: now.addingTimeInterval(3600),
resetDescription: nil),
secondary: nil,
updatedAt: now)
let frozen = try #require(controller.menuCardModel(for: .codex))
monitor.beginManualRefresh(frozenModels: [.codex: frozen], provider: .codex)
defer { monitor.endManualRefresh(for: .codex) }

controller.store.snapshots[.codex] = UsageSnapshot(
primary: RateWindow(
usedPercent: 45,
windowMinutes: 300,
resetsAt: now.addingTimeInterval(3600),
resetDescription: nil),
secondary: RateWindow(
usedPercent: 65,
windowMinutes: 10080,
resetsAt: now.addingTimeInterval(7200),
resetDescription: nil),
updatedAt: now.addingTimeInterval(1))
let refreshed = try #require(controller.menuCardModel(for: .codex))

#expect(!monitor.publishResolvedModelIfCompatible(for: .codex))
#expect(monitor.isManualRefreshInFlight(for: .codex))
#expect(monitor.subtitle(
for: .codex,
fallback: MenuCardLiveSubtitle(text: "old", style: .info)).style == .loading)
#expect(monitor.model(for: .codex, fallback: frozen).metrics.map(\.percent) == frozen.metrics.map(\.percent))

monitor.endManualRefresh(for: .codex)
let reconciled = monitor.model(for: .codex, fallback: refreshed)
#expect(!monitor.isManualRefreshInFlight(for: .codex))
#expect(reconciled.metrics.map(\.percent) == refreshed.metrics.map(\.percent))
if ProcessInfo.processInfo.environment["CODEXBAR_REFRESH_PROBE"] == "1" {
print(
"CODEXBAR_REFRESH_PROBE incompatible coreRows=\(refreshed.metrics.count) " +
"blockedState=refreshing reconciledState=published")
}
}

@Test
func `refresh monitor publishes compatible core error honestly`() throws {
let settings = self.makeSettings()
self.enableOnly([.codex], settings: settings)
let controller = self.makeController(settings: settings)
defer { controller.releaseStatusItemsForTesting() }
let monitor = controller.menuCardRefreshMonitor
let now = Date(timeIntervalSince1970: 1_700_000_000)
controller.store.snapshots[.codex] = UsageSnapshot(
primary: RateWindow(
usedPercent: 20,
windowMinutes: 300,
resetsAt: now.addingTimeInterval(3600),
resetDescription: nil),
secondary: nil,
updatedAt: now)
let frozen = try #require(controller.menuCardModel(for: .codex))
monitor.beginManualRefresh(frozenModels: [.codex: frozen], provider: .codex)
defer { monitor.endManualRefresh(for: .codex) }

controller.store.errors[.codex] = "Synthetic core refresh failure"

#expect(monitor.publishResolvedModelIfCompatible(for: .codex))
#expect(!monitor.isManualRefreshInFlight(for: .codex))
let subtitle = monitor.subtitle(
for: .codex,
fallback: MenuCardLiveSubtitle(text: "old", style: .info))
#expect(subtitle.style == .error)
#expect(monitor.model(for: .codex, fallback: frozen).metrics.map(\.percent) == frozen.metrics.map(\.percent))
}

@Test
func `manual refresh keeps frozen quota even if menu rebuilds before completion`() throws {
let settings = self.makeSettings()
Expand Down
Loading