Skip to content
Closed
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
71 changes: 66 additions & 5 deletions Sources/CodexBar/StatusItemController+SwitcherViews.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1036,6 +1036,7 @@ final class CodexAccountSwitcherView: NSView {
private let accounts: [CodexVisibleAccount]
private let onSelect: (CodexVisibleAccount) -> Void
private var selectedAccountID: String
private var pressedAccountID: String?
private var buttons: [NSButton] = []
private let preferredSize: NSSize
private let rowSpacing: CGFloat = 4
Expand Down Expand Up @@ -1278,10 +1279,48 @@ final class CodexAccountSwitcherView: NSView {
}
}

override func acceptsFirstMouse(for event: NSEvent?) -> Bool {
true
}

override func hitTest(_ point: NSPoint) -> NSView? {
let descendant = super.hitTest(point)
if descendant != nil, descendant !== self {
return self
}
return descendant
}

override func mouseDown(with event: NSEvent) {
let location = self.convert(event.locationInWindow, from: nil)
self.pressedAccountID = self.accountID(at: location)
}

override func mouseUp(with event: NSEvent) {
defer { self.pressedAccountID = nil }
guard let pressedAccountID = self.pressedAccountID else { return }
let location = self.convert(event.locationInWindow, from: nil)
guard let releasedAccountID = self.accountID(at: location),
releasedAccountID == pressedAccountID,
let account = self.accounts.first(where: { $0.id == pressedAccountID })
else {
return
}
self.applySelection(account)
}

private func accountID(at pointInSelf: NSPoint) -> String? {
self.buttons.first(where: { self.convert($0.bounds, from: $0).contains(pointInSelf) })?.identifier?.rawValue
}

@objc private func handleSelect(_ sender: NSButton) {
guard let accountID = sender.identifier?.rawValue else { return }
guard let account = self.accounts.first(where: { $0.id == accountID }) else { return }
self.selectedAccountID = accountID
guard let accountID = sender.identifier?.rawValue,
let account = self.accounts.first(where: { $0.id == accountID }) else { return }
self.applySelection(account)
}

private func applySelection(_ account: CodexVisibleAccount) {
self.selectedAccountID = account.id
self.updateButtonStyles()
self.onSelect(account)
}
Expand All @@ -1296,8 +1335,30 @@ final class CodexAccountSwitcherView: NSView {
}

func _test_selectAccount(id: String) {
guard let button = self.buttons.first(where: { $0.identifier?.rawValue == id }) else { return }
self.handleSelect(button)
guard let account = self.accounts.first(where: { $0.id == id }) else { return }
self.applySelection(account)
}

func _test_simulateRuntimeClick(id: String) -> Bool {
guard let button = self.buttons.first(where: { $0.identifier?.rawValue == id }) else { return false }
let point = self.convert(NSPoint(x: button.bounds.midX, y: button.bounds.midY), from: button)
self.pressedAccountID = self.accountID(at: point)
defer { self.pressedAccountID = nil }
guard let pressedAccountID = self.pressedAccountID,
let releasedAccountID = self.accountID(at: point),
releasedAccountID == pressedAccountID,
let account = self.accounts.first(where: { $0.id == pressedAccountID })
else {
return false
}
self.applySelection(account)
return true
}

func _test_hitTestSwallowsChildButton(id: String) -> Bool {
guard let button = self.buttons.first(where: { $0.identifier?.rawValue == id }) else { return false }
let point = self.convert(NSPoint(x: button.bounds.midX, y: button.bounds.midY), from: button)
return self.hitTest(point) === self
}
#endif
}
50 changes: 50 additions & 0 deletions Sources/CodexBar/StatusItemController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ final class StatusItemController: NSObject, NSMenuDelegate, StatusItemControllin
var providerSwitcherUpdateToken = 0
var lastAppliedMergedIconRenderSignature: String?
var lastAppliedProviderIconRenderSignatures: [UsageProvider: String] = [:]
var lastObservedStoreIconWorkSignature: String?
var lastKnownScreenCount: Int
var pendingScreenChangePreviousCount: Int?
var screenChangeVisibilityTask: Task<Void, Never>?
Expand Down Expand Up @@ -358,11 +359,59 @@ final class StatusItemController: NSObject, NSMenuDelegate, StatusItemControllin
Task { @MainActor [weak self] in
guard let self else { return }
self.observeStoreIconChanges()
let signature = self.storeIconObservationSignature()
guard signature != self.lastObservedStoreIconWorkSignature else { return }
self.lastObservedStoreIconWorkSignature = signature
self.updateIcons()
}
}
}

func storeIconObservationSignature() -> String {
let showBrandPercent = self.settings.menuBarShowsBrandIconWithPercent
let mergeIcons = self.shouldMergeIcons
let needsAnimation = self.needsMenuBarIconAnimation()
let providerSignatures = UsageProvider.allCases.map {
self.providerStoreIconObservationSignature(for: $0, showBrandPercent: showBrandPercent)
}.joined(separator: "||")
let visibleProviders = self.store.enabledProvidersForDisplay().map(\.rawValue).sorted().joined(separator: ",")
return [
"merge=\(mergeIcons ? "1" : "0")",
"visible=\(visibleProviders)",
"iconStyle=\(String(describing: self.store.iconStyle))",
"brandPercent=\(showBrandPercent ? "1" : "0")",
"needsAnimation=\(needsAnimation ? "1" : "0")",
providerSignatures,
].joined(separator: "|")
}

private func providerStoreIconObservationSignature(for provider: UsageProvider, showBrandPercent: Bool) -> String {
let snapshot = self.store.snapshot(for: provider)
let stale = self.store.isStale(provider: provider)
let status = self.store.statusIndicator(for: provider).rawValue
let isVisibleForAnimation = self.shouldMergeIcons ? self.isEnabled(provider) : self.isVisible(provider)
let isAnimating = isVisibleForAnimation && !stale && snapshot == nil
let isRefreshingWarpPlaceholder = self.store.refreshingProviders.contains(provider)
let creditsRemaining = provider == .codex
? self.store.codexMenuBarCreditsRemaining(
snapshotOverride: snapshot,
now: snapshot?.updatedAt ?? Date())
: nil
let displayText = showBrandPercent ? self.menuBarDisplayText(for: provider, snapshot: snapshot) : nil

return [
provider.rawValue,
"style=\(String(describing: self.store.style(for: provider)))",
"snapshot=\(String(describing: snapshot))",
"stale=\(stale ? "1" : "0")",
"status=\(status)",
"anim=\(isAnimating ? "1" : "0")",
"refreshing=\(isRefreshingWarpPlaceholder ? "1" : "0")",
"credits=\(String(describing: creditsRemaining))",
"text=\(displayText ?? "nil")",
].joined(separator: "|")
}

private func observeDebugForceAnimation() {
withObservationTracking {
_ = self.store.debugForceAnimation
Expand Down Expand Up @@ -542,6 +591,7 @@ final class StatusItemController: NSObject, NSMenuDelegate, StatusItemControllin
#if DEBUG
guard !self.isReleasedForTesting else { return }
#endif
self.lastObservedStoreIconWorkSignature = self.storeIconObservationSignature()
// Avoid flicker: when an animation driver is active, store updates can call `updateIcons()` and
// briefly overwrite the animated frame with the static (phase=nil) icon.
let phase: Double? = self.needsMenuBarIconAnimation() ? self.animationPhase : nil
Expand Down
25 changes: 20 additions & 5 deletions Sources/CodexBar/UsageStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ extension UsageStore {
_ = self.openAIDashboard
_ = self.lastOpenAIDashboardError
_ = self.openAIDashboardRequiresLogin
_ = self.isRefreshing
_ = self.refreshingProviders
_ = self.statuses
_ = self.historicalPaceRevision
Expand Down Expand Up @@ -555,7 +554,15 @@ final class UsageStore {
group.addTask { await self.refreshStatus(provider) }
}
}
group.addTask { await self.refreshCreditsIfNeeded(minimumSnapshotUpdatedAt: refreshStartedAt) }
if forceTokenUsage {
group.addTask { await self.refreshCreditsIfNeeded(minimumSnapshotUpdatedAt: refreshStartedAt) }
}
}

if !forceTokenUsage {
Task { @MainActor [weak self] in
await self?.refreshCreditsIfNeeded(minimumSnapshotUpdatedAt: refreshStartedAt)
}
}

if forceTokenUsage {
Expand Down Expand Up @@ -587,9 +594,17 @@ final class UsageStore {
])
if shouldRefreshOpenAIWeb {
let codexDashboardGuard = self.currentCodexOpenAIWebRefreshGuard()
await self.refreshOpenAIDashboardIfNeeded(
force: forceTokenUsage,
expectedGuard: codexDashboardGuard)
if forceTokenUsage {
await self.refreshOpenAIDashboardIfNeeded(
force: true,
expectedGuard: codexDashboardGuard)
} else {
Task { @MainActor [weak self] in
await self?.refreshOpenAIDashboardIfNeeded(
force: false,
expectedGuard: codexDashboardGuard)
}
}
}

if forceTokenUsage, self.openAIDashboardRequiresLogin {
Expand Down
Loading