import AppKit import CodexBarCore import Observation import QuartzCore // MARK: - Status item controller (AppKit-hosted icons, SwiftUI popovers) @MainActor protocol StatusItemControlling: AnyObject { func openMenuFromShortcut() func runLoginFlowFromSettings(provider: UsageProvider) async func celebrationOriginPoint(for provider: UsageProvider?) -> CGPoint? func trimRebuildableCachesForMemoryPressure() -> MemoryPressureCacheTrimSummary #if DEBUG func seedRebuildableCachesForMemoryPressureProof() #endif func prepareForAppShutdown() } extension StatusItemControlling { func celebrationOriginPoint(for provider: UsageProvider?) -> CGPoint? { nil } func trimRebuildableCachesForMemoryPressure() -> MemoryPressureCacheTrimSummary { MemoryPressureCacheTrimSummary() } #if DEBUG func seedRebuildableCachesForMemoryPressureProof() {} #endif func prepareForAppShutdown() {} } struct NativeHighlightDeferredMenuRebuild { let provider: UsageProvider? } @MainActor final class StatusItemController: NSObject, NSMenuDelegate, StatusItemControlling { // Disable SwiftUI menu cards + menu refresh work in tests to avoid swiftpm-testing-helper crashes. static var menuCardRenderingEnabled = !SettingsStore.isRunningTests private static let defaultMenuRefreshEnabled = !SettingsStore.isRunningTests private(set) static var menuRefreshEnabled = !SettingsStore.isRunningTests static let quotaWarningFlashDuration: TimeInterval = 60 private nonisolated static let statusItemAccessibilityTitle = "CodexBar" private nonisolated static let debugStatusItemAccessibilityTitle = "CodexBar Debug" private nonisolated static let statusItemAccessibilityIdentifierPrefix = "CodexBar.StatusItem" private nonisolated static let mergedLegacyDefaultItemIndex = 0 enum StatusItemIdentity { case merged case provider(UsageProvider) var autosaveName: String { switch self { case .merged: "codexbar-merged" case let .provider(provider): "codexbar-\(provider.rawValue)" } } var accessibilityIdentifier: String { switch self { case .merged: StatusItemController.statusItemAccessibilityIdentifierPrefix case let .provider(provider): "\(StatusItemController.statusItemAccessibilityIdentifierPrefix).\(provider.rawValue)" } } } nonisolated static func isDebugApp(bundleIdentifier: String?) -> Bool { bundleIdentifier?.contains(".debug") == true } nonisolated static func statusItemAccessibilityTitle(isDebugApp: Bool) -> String { isDebugApp ? self.debugStatusItemAccessibilityTitle : self.statusItemAccessibilityTitle } #if DEBUG var menuRefreshEnabledOverrideForTesting: Bool? #endif typealias Factory = @MainActor ( UsageStore, SettingsStore, AccountInfo, UpdaterProviding, PreferencesSelection, ManagedCodexAccountCoordinator, CodexAccountPromotionCoordinator) -> StatusItemControlling // swiftlint:disable:next function_parameter_count static func makeDefaultController( store: UsageStore, settings: SettingsStore, account: AccountInfo, updater: UpdaterProviding, selection: PreferencesSelection, managedCodexAccountCoordinator: ManagedCodexAccountCoordinator, codexAccountPromotionCoordinator: CodexAccountPromotionCoordinator) -> StatusItemControlling { StatusItemController( store: store, settings: settings, account: account, updater: updater, preferencesSelection: selection, managedCodexAccountCoordinator: managedCodexAccountCoordinator, codexAccountPromotionCoordinator: codexAccountPromotionCoordinator) } static let defaultFactory: Factory = StatusItemController.makeDefaultController static var factory: Factory = StatusItemController.defaultFactory let store: UsageStore let settings: SettingsStore let agentSessions: AgentSessionsStore lazy var menuCardRefreshMonitor = self.makeMenuCardRefreshMonitor() let account: AccountInfo let updater: UpdaterProviding let managedCodexAccountCoordinator: ManagedCodexAccountCoordinator let codexAccountPromotionCoordinator: CodexAccountPromotionCoordinator let statusBar: NSStatusBar let menuCardRenderingEnabledForController: Bool let menuRefreshEnabledForController: Bool var statusItem: NSStatusItem var statusItems: [UsageProvider: NSStatusItem] = [:] /// App intent survives Tahoe changing `NSStatusItem.isVisible` after Control Center rejects its scene. var expectedVisibleStatusItemAutosaveNames: Set = [] var lastMenuProvider: UsageProvider? var menuProviders: [ObjectIdentifier: UsageProvider] = [:] var menuSession = MenuSessionCoordinator() var menuReadinessSignatures: [ObjectIdentifier: String] = [:] let hostedSubviewRenderSignatures = NSMapTable.weakToStrongObjects() /// Persistent Refresh rows are weakly tracked so their enabled state can change during menu tracking. let persistentRefreshItems = NSHashTable.weakObjects() var menuCardHeightCache: [MenuCardHeightCacheKey: CGFloat] = [:] var measuredStandardMenuWidthCache: [String: CGFloat] = [:] var lastMenuAdjunctReadinessSignature = "" var lastMenuAdjunctReadinessBaselineVersion = 0 var rootOpenHandledMenuObservationSignature: String? var mergedMenu: NSMenu? var providerMenus: [UsageProvider: NSMenu] = [:] var fallbackMenu: NSMenu? var openMenus: [ObjectIdentifier: NSMenu] = [:] var menuRefreshTasks: [ObjectIdentifier: Task] = [:] /// Manual refreshes tracked per scope so refreshing one provider neither greys out nor blocks /// a manual refresh of another. `.global` covers the all-providers refresh (⌘R / merged overview). var manualRefreshTasks: [ManualRefreshScope: Task] = [:] var closedMenuRebuildTasks: [ObjectIdentifier: Task] = [:] var closedMenuRebuildRequests = MenuRebuildRequestRegistry() var openMenuRebuildTasks: [ObjectIdentifier: Task] = [:] var openMenuRebuildRequests = MenuRebuildRequestRegistry() var menuIdentitySignatures: [ObjectIdentifier: String] = [:] var codexAccountMenuProjectionRevalidationTask: Task? var openMenuRebuildsClosingHostedSubviewMenus: Set = [] var parentMenuRebuildPendingAfterHostedSubviewClose = false var deferredMenuInteractionRefreshProviders: Set = [] var deferredMenuInteractionRefreshPending: Bool { !self.deferredMenuInteractionRefreshProviders.isEmpty } var deferredOpenAIDashboardRefreshReason: String? var deferredMenuInteractionRefreshTask: Task? var highlightedMenuItems: [ObjectIdentifier: NSMenuItem] = [:] /// Open-menu rebuilds paused so AppKit's native selection background cannot retain stale geometry. var nativeHighlightDeferredMenuRebuilds: [ObjectIdentifier: NativeHighlightDeferredMenuRebuild] = [:] /// Baseline resync intent survives rebuild coalescing and any native-row or hosted-submenu deferral. var pendingMenuBaselineResyncs: Set = [] var providerSwitcherShortcutEventMonitor: ProviderSwitcherShortcutEventMonitor? var providerSwitcherShortcutMenuID: ObjectIdentifier? var providerSwitcherPointerInteractionMenuID: ObjectIdentifier? var pendingProviderSwitcherPointerRebuild: PendingProviderSwitcherRebuild? var overviewScrollAccumulatedDelta: CGFloat = 0 var overviewScrollNavigationHandlerForTesting: ((OverviewScrollStep) -> Void)? var hasPreparedForAppShutdown = false var scheduleQuitTermination: (@escaping @MainActor () -> Void) -> Void = { operation in DispatchQueue.main.async { Task { @MainActor in operation() } } } var terminateApplicationForQuit: @MainActor () -> Void = { NSApp.terminate(nil) } var openMenuInvalidationRetryTask: Task? #if DEBUG var onDelayedMenuRefreshAttemptForTesting: (() -> Void)? var onDeferredMenuInteractionRefreshForTesting: (() -> Void)? var onOpenMenuInvalidationRetryForTesting: (() -> Void)? var isReleasedForTesting = false var lastLoggedClosedMenuRebuildVersion: Int? var _test_openMenuRefreshYieldOverride: (@MainActor () async -> Void)? var _test_openMenuRebuildObserver: (@MainActor (NSMenu) -> Void)? var _test_providerSwitcherMenuRebuildDebounceNanoseconds: UInt64? var _test_codexAmbientLoginRunnerOverride: (@MainActor (TimeInterval) async -> CodexLoginRunner.Result)? #endif var manualRefreshViewportRestoreState = ManualRefreshViewportRestoreState() var blinkTask: Task? var menuBarCountdownRefreshTask: Task? var loginTask: Task? { didSet { self.refreshMenusForLoginStateChange() } } var creditsPurchaseWindow: OpenAICreditsPurchaseWindowController? var activeLoginProvider: UsageProvider? { didSet { if oldValue != self.activeLoginProvider { self.refreshMenusForLoginStateChange() } } } var blinkStates: [UsageProvider: BlinkState] = [:] var blinkAmounts: [UsageProvider: CGFloat] = [:] var wiggleAmounts: [UsageProvider: CGFloat] = [:] var tiltAmounts: [UsageProvider: CGFloat] = [:] var quotaWarningFlashUntil: [UsageProvider: Date] = [:] var quotaWarningFlashTasks: [UsageProvider: Task] = [:] var blinkForceUntil: Date? var loginPhase: LoginPhase = .idle { didSet { if oldValue != self.loginPhase { self.refreshMenusForLoginStateChange() } } } let preferencesSelection: PreferencesSelection var animationDriver: DisplayLinkDriver? var animationPhase: Double = 0 var animationPattern: LoadingPattern = .knightRider var animationStartedAt: Date? private var lastConfigRevision: Int private var lastProviderOrder: [UsageProvider] private var lastMergeIcons: Bool private var lastSwitcherShowsIcons: Bool private var lastObservedUsageBarsShowUsed: Bool var lastWidgetDisplaySettingsSignature = "" private var lastAgentSessionsEnabled: Bool private var lastAgentSessionsManualHosts: String /// Tracks which `usageBarsShowUsed` mode the provider switcher was built with. /// Used to decide whether we can "smart update" menu content without rebuilding the switcher. var lastSwitcherUsageBarsShowUsed: Bool /// Tracks whether the merged-menu switcher was built with the Overview tab visible. /// Used to force switcher rebuilds when Overview availability toggles. var lastSwitcherIncludesOverview: Bool = false /// Tracks localization-sensitive labels used by the merged menu. /// Used to force menu rebuilds when app language changes. var lastMenuLocalizationSignature: String = "" /// Tracks which providers the merged menu's switcher was built with, to detect when it needs full rebuild. var lastSwitcherProviders: [UsageProvider] = [] /// Tracks which switcher tab state was used for the current merged-menu switcher instance. var lastMergedSwitcherSelection: ProviderSwitcherSelection? /// Tracks which provider/overview content is currently attached below the merged-menu switcher. var lastMergedMenuContentSelection: ProviderSwitcherSelection? /// Tracks the visible Codex account switcher contents for merged-menu smart updates. var lastCodexAccountMenuDisplay: CodexAccountMenuDisplay? /// Tracks the visible token account switcher contents for merged-menu smart updates. var lastTokenAccountMenuDisplay: TokenAccountMenuDisplay? /// Keeps detached merged-menu tab content reusable while the same menu remains open. var mergedSwitcherContentCaches: [ObjectIdentifier: [ProviderSwitcherSelection: CachedMergedSwitcherMenuContent]] = [:] var preservesMergedSwitcherContentCachesDuringInvalidation = false /// Card hosting views harvested from items about to be discarded by the current populate /// pass, keyed by card identifier; consumed by `makeMenuCardItem` and cleared when the /// pass finishes. Never outlives a single synchronous menu population. var menuCardViewRecyclePool: [String: NSView] = [:] /// Monotonic token used to ignore stale deferred provider-switcher menu rebuilds. var providerSwitcherUpdateToken = 0 var providerSelectionUIRefreshTask: Task? var deferredMergedIconRenderAfterTracking = false var lastAppliedMergedIconRenderSignature: String? var lastAppliedProviderIconRenderSignatures: [UsageProvider: String] = [:] var lastObservedStoreIconWorkSignature: String? var iconPerfRefreshCycleMetrics: IconPerfRefreshCycleMetrics? var iconPerfUpdatePassActive = false var lastKnownScreenCount: Int var pendingScreenChangePreviousCount: Int? var screenChangeVisibilityTask: Task? let loginLogger = CodexBarLog.logger(LogCategories.login) let menuLogger = CodexBarLog.logger(LogCategories.app) var selectedMenuProvider: UsageProvider? { get { self.settings.selectedMenuProvider } set { self.settings.selectedMenuProvider = newValue } } private static func makeStatusItem( statusBar: NSStatusBar, identity: StatusItemIdentity, defaults: UserDefaults, legacyDefaultItemIndex: Int?) -> NSStatusItem { MenuBarStatusItemPlacementPreflight.prepare( defaults: defaults, autosaveName: identity.autosaveName, legacyDefaultItemIndex: legacyDefaultItemIndex) let item = statusBar.statusItem(withLength: NSStatusItem.variableLength) item.autosaveName = identity.autosaveName if let button = item.button { let title = self.statusItemAccessibilityTitle( isDebugApp: self.isDebugApp(bundleIdentifier: Bundle.main.bundleIdentifier)) // Ensure the icon is rendered at 1:1 without resampling (crisper edges for template images). button.imageScaling = .scaleNone button.setAccessibilityIdentifier(identity.accessibilityIdentifier) button.setAccessibilityTitle(title) button.toolTip = title } return item } struct BlinkState { var nextBlink: Date var blinkStart: Date? var pendingSecondStart: Date? var effect: MotionEffect = .blink static func randomDelay() -> TimeInterval { Double.random(in: 3...12) } } enum MotionEffect { case blink case wiggle case tilt } enum LoginPhase { case idle case requesting case waitingBrowser } func menuBarMetricWindow(for provider: UsageProvider, snapshot: UsageSnapshot?, now: Date = Date()) -> RateWindow? { if provider == .codex { return self.codexMenuBarMetricWindow(snapshot: snapshot, now: now) } return MenuBarMetricWindowResolver.rateWindow( preference: self.settings.menuBarMetricPreference(for: provider, snapshot: snapshot), provider: provider, snapshot: snapshot, supportsAverage: self.settings.menuBarMetricSupportsAverage(for: provider)) } private func codexMenuBarMetricWindow(snapshot: UsageSnapshot?, now: Date) -> RateWindow? { guard let snapshot else { return nil } return self.store.codexMenuBarMetricWindow(snapshot: snapshot, now: now) } init( store: UsageStore, settings: SettingsStore, account: AccountInfo, updater: UpdaterProviding, preferencesSelection: PreferencesSelection, managedCodexAccountCoordinator: ManagedCodexAccountCoordinator = ManagedCodexAccountCoordinator(), codexAccountPromotionCoordinator: CodexAccountPromotionCoordinator? = nil, statusBar: NSStatusBar = .system, menuCardRenderingEnabled: Bool = StatusItemController.menuCardRenderingEnabled, menuRefreshEnabled: Bool = StatusItemController.menuRefreshEnabled, observeProviderConfigNotifications: Bool = !SettingsStore.isRunningTests) { if SettingsStore.isRunningTests { _ = NSApplication.shared } self.store = store self.settings = settings self.agentSessions = AgentSessionsStore(settings: settings) self.account = account self.updater = updater self.preferencesSelection = preferencesSelection self.managedCodexAccountCoordinator = managedCodexAccountCoordinator self.codexAccountPromotionCoordinator = codexAccountPromotionCoordinator ?? CodexAccountPromotionCoordinator( settingsStore: settings, usageStore: store, managedAccountCoordinator: managedCodexAccountCoordinator) self.lastConfigRevision = settings.configRevision self.lastProviderOrder = settings.providerOrder self.lastMergeIcons = settings.mergeIcons self.lastSwitcherShowsIcons = settings.switcherShowsIcons self.lastObservedUsageBarsShowUsed = settings.usageBarsShowUsed self.lastAgentSessionsEnabled = settings.agentSessionsEnabled self.lastAgentSessionsManualHosts = settings.agentSessionsManualHosts self.lastSwitcherUsageBarsShowUsed = settings.usageBarsShowUsed self.menuCardRenderingEnabledForController = menuCardRenderingEnabled self.menuRefreshEnabledForController = menuRefreshEnabled let repairedStatusItemVisibilityKeys = MenuBarStatusItemDefaultsRepair .repairHiddenVisibilityDefaultsIfNeeded(defaults: settings.userDefaults) self.statusBar = statusBar self.statusItem = Self.makeStatusItem( statusBar: statusBar, identity: .merged, defaults: settings.userDefaults, legacyDefaultItemIndex: Self.mergedLegacyDefaultItemIndex) self.lastKnownScreenCount = NSScreen.screens.count // Status items for individual providers are now created lazily in updateVisibility() super.init() if !repairedStatusItemVisibilityKeys.isEmpty { self.menuLogger.info( "Repaired hidden macOS status-item visibility defaults", metadata: ["keys": repairedStatusItemVisibilityKeys.joined(separator: ",")]) } self.lastMenuAdjunctReadinessSignature = self.menuAdjunctReadinessSignature() self.lastMenuAdjunctReadinessBaselineVersion = self.menuSession.contentVersion self.lastWidgetDisplaySettingsSignature = self.widgetDisplaySettingsSignature() self.wireBindings() self.agentSessions.onUpdate = { [weak self] in self?.invalidateMenus(refreshOpenMenus: true) } if !SettingsStore.isRunningTests { self.agentSessions.start() } self.updateVisibility() self.updateIcons() self.scheduleCodexAccountMenuProjectionRevalidationIfNeeded( for: self.store.enabledProvidersForDisplay()) self.scheduleStartupStatusItemVisibilityCheck() NotificationCenter.default.addObserver( self, selector: #selector(self.handleDebugReplayNotification(_:)), name: .codexbarDebugReplayAllAnimations, object: nil) NotificationCenter.default.addObserver( self, selector: #selector(self.handleDebugBlinkNotification), name: .codexbarDebugBlinkNow, object: nil) NotificationCenter.default.addObserver( self, selector: #selector(self.handleQuotaWarningPosted(_:)), name: .codexbarQuotaWarningDidPost, object: nil) if observeProviderConfigNotifications { NotificationCenter.default.addObserver( self, selector: #selector(self.handleProviderConfigDidChange), name: .codexbarProviderConfigDidChange, object: nil) } NotificationCenter.default.addObserver( self, selector: #selector(self.handleScreenParametersDidChange(_:)), name: NSApplication.didChangeScreenParametersNotification, object: nil) self.observeMenuBarTimeEnvironmentChanges() } convenience init( store: UsageStore, settings: SettingsStore, account: AccountInfo, updater: UpdaterProviding, preferencesSelection: PreferencesSelection, statusBar: NSStatusBar = .system, menuCardRenderingEnabled: Bool = StatusItemController.menuCardRenderingEnabled, menuRefreshEnabled: Bool = StatusItemController.menuRefreshEnabled, observeProviderConfigNotifications: Bool = !SettingsStore.isRunningTests) { self.init( store: store, settings: settings, account: account, updater: updater, preferencesSelection: preferencesSelection, managedCodexAccountCoordinator: ManagedCodexAccountCoordinator(), codexAccountPromotionCoordinator: nil, statusBar: statusBar, menuCardRenderingEnabled: menuCardRenderingEnabled, menuRefreshEnabled: menuRefreshEnabled, observeProviderConfigNotifications: observeProviderConfigNotifications) } private func wireBindings() { self.observeStoreChanges() self.observeStoreIconChanges() self.observeIconPerfRefreshCycleChanges() self.observeDebugForceAnimation() self.observeSettingsChanges() self.observeUpdaterChanges() self.observeManagedCodexCoordinatorChanges() } private func observeStoreChanges() { withObservationTracking { _ = self.store.menuObservationToken } onChange: { [weak self] in Task { @MainActor [weak self] in guard let self else { return } self.handleObservedStoreMenuChange() } } } func handleObservedStoreMenuChange() { self.observeStoreChanges() self.updatePersistentRefreshItemsEnabled() let rootOpenHandledReadiness = self.consumeRootOpenHandledMenuObservationIfNeeded() // `refreshOpenMenus` is only consulted when a menu is currently open. // Computing the readiness signature serializes every enabled provider's // token snapshot and 30-day daily breakdown, which is wasted main-thread // work on the common path where no menu is open (background refresh ticks). let refreshOpenMenus = self.openMenus.isEmpty ? false : rootOpenHandledReadiness || self.didMenuAdjunctReadinessChange() self.invalidateMenus( refreshOpenMenus: refreshOpenMenus, deferOpenParentMenuRebuild: true, allowStaleContentDuringDataRefresh: true) self.completeParentMenuRebuildAfterHostedSubviewCloseIfNeeded() } private func observeStoreIconChanges() { withObservationTracking { _ = self.store.iconObservationToken } onChange: { [weak self] in Task { @MainActor [weak self] in guard let self else { return } self.observeStoreIconChanges() let signature = self.storeIconObservationSignature() guard signature != self.lastObservedStoreIconWorkSignature else { return } // Reuse the signature we just computed for the change check; `updateIcons` would // otherwise recompute the identical value on the same main-actor turn. self.updateIcons(precomputedStoreIconSignature: signature) } } } private func observeDebugForceAnimation() { withObservationTracking { _ = self.store.debugForceAnimation } onChange: { [weak self] in Task { @MainActor [weak self] in guard let self else { return } self.observeDebugForceAnimation() self.updateVisibility() self.updateBlinkingState() } } } private func observeSettingsChanges() { withObservationTracking { _ = self.settings.menuObservationToken } onChange: { [weak self] in Task { @MainActor [weak self] in guard let self else { return } self.observeSettingsChanges() self.handleSettingsChange(reason: "observation") } } } func handleProviderConfigChange(reason: String) { self.handleSettingsChange(reason: "config:\(reason)") } @objc private func handleProviderConfigDidChange(_ notification: Notification) { #if DEBUG guard !self.isReleasedForTesting else { return } #endif let reason = notification.userInfo?["reason"] as? String ?? "unknown" let affectsBackgroundWork = notification.userInfo?["affectsBackgroundWork"] as? Bool if let source = notification.object as? SettingsStore, source !== self.settings { if let config = notification.userInfo?["config"] as? CodexBarConfig { self.settings.applyExternalConfig( config, reason: "external-\(reason)", affectsBackgroundWork: affectsBackgroundWork) } else { self.settings.reloadConfig( reason: "external-\(reason)", affectsBackgroundWork: affectsBackgroundWork) } } self.handleProviderConfigChange(reason: "notification:\(reason)") } @objc private func handleQuotaWarningPosted(_ notification: Notification) { guard let event = notification.object as? QuotaWarningPostedEvent else { return } self.startQuotaWarningFlash(provider: event.provider, postedAt: event.postedAt) } private func observeUpdaterChanges() { withObservationTracking { _ = self.updater.updateStatus.isUpdateReady } onChange: { [weak self] in Task { @MainActor [weak self] in guard let self else { return } self.observeUpdaterChanges() self.invalidateMenus() } } } private func observeManagedCodexCoordinatorChanges() { withObservationTracking { _ = self.managedCodexAccountCoordinator.isAuthenticatingManagedAccount _ = self.managedCodexAccountCoordinator.authenticatingManagedAccountID _ = self.managedCodexAccountCoordinator.isRemovingManagedAccount _ = self.managedCodexAccountCoordinator.removingManagedAccountID _ = self.codexAccountPromotionCoordinator.isAuthenticatingLiveAccount _ = self.codexAccountPromotionCoordinator.isPromotingSystemAccount } onChange: { [weak self] in Task { @MainActor [weak self] in guard let self else { return } self.observeManagedCodexCoordinatorChanges() self.refreshMenusForLoginStateChange() } } } private func shouldRefreshOpenMenusForProviderSwitcher() -> Bool { var shouldRefresh = false let revision = self.settings.configRevision if revision != self.lastConfigRevision { self.lastConfigRevision = revision shouldRefresh = true } let order = self.settings.providerOrder if order != self.lastProviderOrder { self.lastProviderOrder = order shouldRefresh = true } let mergeIcons = self.settings.mergeIcons if mergeIcons != self.lastMergeIcons { self.lastMergeIcons = mergeIcons shouldRefresh = true } let showsIcons = self.settings.switcherShowsIcons if showsIcons != self.lastSwitcherShowsIcons { self.lastSwitcherShowsIcons = showsIcons shouldRefresh = true } let usageBarsShowUsed = self.settings.usageBarsShowUsed if usageBarsShowUsed != self.lastObservedUsageBarsShowUsed { self.lastObservedUsageBarsShowUsed = usageBarsShowUsed shouldRefresh = true } if self.menuLocalizationSignature() != self.lastMenuLocalizationSignature { shouldRefresh = true } return shouldRefresh } private func handleSettingsChange(reason: String) { #if DEBUG guard !self.isReleasedForTesting else { return } #endif let agentSessionsSettingsChanged = self.settings.agentSessionsEnabled != self.lastAgentSessionsEnabled || self.settings.agentSessionsManualHosts != self.lastAgentSessionsManualHosts if agentSessionsSettingsChanged { self.lastAgentSessionsEnabled = self.settings.agentSessionsEnabled self.lastAgentSessionsManualHosts = self.settings.agentSessionsManualHosts self.agentSessions.settingsDidChange() } let configChanged = self.settings.configRevision != self.lastConfigRevision let orderChanged = self.settings.providerOrder != self.lastProviderOrder let localizationChanged = self.menuLocalizationSignature() != self.lastMenuLocalizationSignature let shouldRefreshOpenMenus = self.shouldRefreshOpenMenusForProviderSwitcher() self.invalidateMenus() if orderChanged || configChanged { self.rebuildProviderStatusItems() } self.updateVisibility() self.updateIcons() self.persistWidgetSnapshotIfWidgetDisplaySettingsChanged() if shouldRefreshOpenMenus { self.refreshOpenMenusAllowingParentRebuild( deferParentRebuildDuringTracking: !localizationChanged) } } /// Updates the menu bar icons. /// /// The store-icon observer already computes `storeIconObservationSignature()` to decide whether any /// icon work is needed, so it passes that value in via `precomputedStoreIconSignature` to avoid /// recomputing the identical signature on the same main-actor turn. Other callers omit it and let the /// signature refresh here, keeping `lastObservedStoreIconWorkSignature` current as the change gate. func updateIcons(precomputedStoreIconSignature: String? = nil) { #if DEBUG guard !self.isReleasedForTesting else { return } #endif MainThreadActivityBreadcrumb.push("updateIcons") self.scheduleMenuBarCountdownRefreshIfNeeded() self.lastObservedStoreIconWorkSignature = precomputedStoreIconSignature ?? self.storeIconObservationSignature() self.beginIconPerfUpdatePass() defer { self.endIconPerfUpdatePass() MainThreadActivityBreadcrumb.pop() } // 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 if self.shouldMergeIcons { let skippedMergedRender = self.applyIcon(phase: phase) if skippedMergedRender, !self.deferredMergedIconRenderAfterTracking, self.mergedMenu != nil { return } guard !self.isMergedMenuOpen else { self.updateAnimationState() self.updateBlinkingState() return } self.attachMenus() } else { UsageProvider.allCases.forEach { self.applyIcon(for: $0, phase: phase) } self.attachMenus(fallback: self.fallbackProvider) } self.updateAnimationState() self.updateBlinkingState() } var isMergedMenuOpen: Bool { guard let mergedMenu else { return false } return self.openMenus[ObjectIdentifier(mergedMenu)] != nil } /// Lazily retrieves or creates a status item for the given provider func lazyStatusItem(for provider: UsageProvider) -> NSStatusItem { if let existing = self.statusItems[provider] { return existing } let item = Self.makeStatusItem( statusBar: self.statusBar, identity: .provider(provider), defaults: self.settings.userDefaults, legacyDefaultItemIndex: self.legacyDefaultItemIndex(forNewProvider: provider)) self.statusItems[provider] = item return item } func recreateStatusItemsForVisibilityRecovery() { #if DEBUG guard !self.isReleasedForTesting else { return } #endif self.statusItem.menu = nil self.statusBar.removeStatusItem(self.statusItem) self.statusItem = Self.makeStatusItem( statusBar: self.statusBar, identity: .merged, defaults: self.settings.userDefaults, legacyDefaultItemIndex: Self.mergedLegacyDefaultItemIndex) for provider in Array(self.statusItems.keys) { self.removeProviderStatusItem(for: provider) } self.lastAppliedMergedIconRenderSignature = nil self.lastAppliedProviderIconRenderSignatures.removeAll() self.updateVisibility() self.updateIcons() } private func updateVisibility() { #if DEBUG guard !self.isReleasedForTesting else { return } #endif let anyEnabled = !self.store.enabledProvidersForDisplay().isEmpty let force = self.store.debugForceAnimation let mergeIcons = self.shouldMergeIcons var expectedVisibleAutosaveNames: Set = [] if mergeIcons { let shouldBeVisible = anyEnabled || force self.statusItem.isVisible = shouldBeVisible if shouldBeVisible { expectedVisibleAutosaveNames.insert(self.statusItem.autosaveName) } for provider in Array(self.statusItems.keys) { self.removeProviderStatusItem(for: provider) } self.attachMenus() } else { self.statusItem.isVisible = false let fallback = self.fallbackProvider for provider in self.settings.orderedProviders() { let isEnabled = self.isEnabled(provider) let shouldBeVisible = isEnabled || fallback == provider || force if shouldBeVisible { let item = self.lazyStatusItem(for: provider) item.isVisible = true expectedVisibleAutosaveNames.insert(item.autosaveName) } else { self.removeProviderStatusItem(for: provider) } } self.attachMenus(fallback: fallback) } self.expectedVisibleStatusItemAutosaveNames = expectedVisibleAutosaveNames self.updateAnimationState() self.updateBlinkingState() } func isEnabled(_ provider: UsageProvider) -> Bool { self.store.isEnabled(provider) } private func refreshMenusForLoginStateChange() { #if DEBUG guard !self.isReleasedForTesting else { return } #endif self.invalidateMenus() if self.shouldMergeIcons { guard !self.isMergedMenuOpen else { return } self.attachMenus() } else { self.attachMenus(fallback: self.fallbackProvider) } } private func attachMenus() { if self.mergedMenu == nil { self.mergedMenu = self.makeMenu() } if self.statusItem.menu !== self.mergedMenu { self.statusItem.menu = self.mergedMenu } self.prepareAttachedClosedMenusIfNeeded() } private func attachMenus(fallback: UsageProvider? = nil) { for provider in UsageProvider.allCases { // Only access/create the status item if it's actually needed let shouldHaveItem = self.isEnabled(provider) || fallback == provider if shouldHaveItem { let item = self.lazyStatusItem(for: provider) if self.isEnabled(provider) { if self.providerMenus[provider] == nil { self.providerMenus[provider] = self.makeMenu(for: provider) } let menu = self.providerMenus[provider] if item.menu !== menu { item.menu = menu } } else if fallback == provider { if self.fallbackMenu == nil { self.fallbackMenu = self.makeMenu(for: nil) } if item.menu !== self.fallbackMenu { item.menu = self.fallbackMenu } } } else if let item = self.statusItems[provider] { item.menu = nil } } self.prepareAttachedClosedMenusIfNeeded() } private func rebuildProviderStatusItems() { #if DEBUG guard !self.isReleasedForTesting else { return } #endif let ordered = self.settings.orderedProviders() let desired = Set(ordered) for provider in Array(self.statusItems.keys) where !desired.contains(provider) { self.removeProviderStatusItem(for: provider) } guard !self.shouldMergeIcons else { return } let fallback = self.fallbackProvider let force = self.store.debugForceAnimation for provider in ordered where self.isEnabled(provider) || fallback == provider || force { _ = self.lazyStatusItem(for: provider) } } private func removeProviderStatusItem(for provider: UsageProvider) { if let menu = self.providerMenus.removeValue(forKey: provider) { let menuID = ObjectIdentifier(menu) if menuID == self.providerSwitcherShortcutMenuID { self.removeProviderSwitcherShortcutMonitor() } self.clearMergedSwitcherContentCache(for: menu) self.removeMenuLifecycleState(menuID) } guard let item = self.statusItems.removeValue(forKey: provider) else { return } item.menu = nil self.lastAppliedProviderIconRenderSignatures.removeValue(forKey: provider) self.statusBar.removeStatusItem(item) } func isVisible(_ provider: UsageProvider) -> Bool { self.store.debugForceAnimation || self.isEnabled(provider) || self.fallbackProvider == provider } var shouldMergeIcons: Bool { self.settings.mergeIcons && self.store.enabledProvidersForDisplay().count > 1 } func switchAccountSubtitle(for target: UsageProvider) -> String? { guard self.loginTask != nil, let provider = self.activeLoginProvider, provider == target else { return nil } let base: String switch self.loginPhase { case .idle: return nil case .requesting: base = L("Requesting login…") case .waitingBrowser: base = L("Waiting in browser…") } let prefix = ProviderDescriptorRegistry.descriptor(for: provider).metadata.displayName return "\(prefix): \(base)" } deinit { let animationDriver = self.animationDriver Task { @MainActor in animationDriver?.stop() } self.blinkTask?.cancel() self.menuBarCountdownRefreshTask?.cancel() self.loginTask?.cancel() self.screenChangeVisibilityTask?.cancel() self.pendingScreenChangePreviousCount = nil NotificationCenter.default.removeObserver(self) } } #if DEBUG extension StatusItemController { var _test_manualRefreshOperation: (@MainActor () async -> Void)? { get { self.manualRefreshViewportRestoreState.testOperation } set { self.manualRefreshViewportRestoreState.testOperation = newValue } } var _test_menuViewportRestoreObserver: (@MainActor (NSMenu) -> Void)? { get { self.manualRefreshViewportRestoreState.testObserver } set { self.manualRefreshViewportRestoreState.testObserver = newValue } } var _test_menuViewportRestoreScheduler: ((@escaping @MainActor () -> Void) -> Void)? { get { self.manualRefreshViewportRestoreState.testScheduler } set { self.manualRefreshViewportRestoreState.testScheduler = newValue } } var menuContentVersion: Int { get { self.menuSession.contentVersion } set { self.menuSession.replaceContentVersionForTesting(newValue) } } var latestRequiredMenuRebuildVersion: Int { self.menuSession.latestRequiredRebuildVersion } var latestDataOnlyMenuContentVersion: Int { self.menuSession.latestDataOnlyContentVersion } var latestStructuralMenuContentVersion: Int { self.menuSession.latestStructuralContentVersion } var menuVersions: [ObjectIdentifier: Int] { get { self.menuSession.renderedVersions } set { self.menuSession.replaceRenderedVersionsForTesting(newValue) } } var closedMenusDeferredUntilNextOpen: Set { get { self.menuSession.deferredUntilNextOpen } set { self.menuSession.replaceDeferredMenusForTesting(newValue) } } var parentMenuRebuildsDeferredDuringTracking: Set { self.menuSession.parentRebuildsDeferredDuringTracking } var closedMenuRebuildTokens: [ObjectIdentifier: Int] { self.closedMenuRebuildRequests.tokens } } #endif #if DEBUG extension StatusItemController { static func setMenuRefreshEnabledForTesting(_ enabled: Bool) { self.menuRefreshEnabled = enabled } static func resetMenuRefreshEnabledForTesting() { self.menuRefreshEnabled = self.defaultMenuRefreshEnabled } } #endif extension StatusItemController { private func legacyDefaultItemIndex(forNewProvider provider: UsageProvider) -> Int? { let visibleProviders = self.settings.orderedProviders().filter { self.isVisible($0) } guard let providerOffset = visibleProviders.firstIndex(of: provider) else { return nil } return Self.mergedLegacyDefaultItemIndex + 1 + providerOffset } func refreshExistingStatusItemsForVisibilityRecovery() { #if DEBUG guard !self.isReleasedForTesting else { return } #endif let visibleItems = ([self.statusItem] + Array(self.statusItems.values)).filter(\.isVisible) for item in visibleItems { item.isVisible = false } for item in visibleItems { item.isVisible = true } self.updateVisibility() self.updateIcons() } }