1266 lines · 50.2 KB · Swift
RawDownload
import AppKitimport CodexBarCoreimport Observationimport QuartzCoreimport SwiftUI // MARK: - NSMenu construction extension StatusItemController {    private static let menuCardBaseWidth: CGFloat = 310    private static let menuOpenRefreshDelay: Duration = .seconds(1.2)    private struct OpenAIWebMenuItems {        let hasUsageBreakdown: Bool        let hasCreditsHistory: Bool        let hasCostHistory: Bool    }     private struct TokenAccountMenuDisplay {        let provider: UsageProvider        let accounts: [ProviderTokenAccount]        let snapshots: [TokenAccountUsageSnapshot]        let activeIndex: Int        let showAll: Bool        let showSwitcher: Bool    }     private func menuCardWidth(for providers: [UsageProvider], menu: NSMenu? = nil) -> CGFloat {        _ = menu        return Self.menuCardBaseWidth    }     func makeMenu() -> NSMenu {        guard self.shouldMergeIcons else {            return self.makeMenu(for: nil)        }        let menu = NSMenu()        menu.autoenablesItems = false        menu.delegate = self        return menu    }     func menuWillOpen(_ menu: NSMenu) {        if self.isHostedSubviewMenu(menu) {            self.refreshHostedSubviewHeights(in: menu)            if Self.menuRefreshEnabled, self.isOpenAIWebSubviewMenu(menu) {                self.store.requestOpenAIDashboardRefreshIfStale(reason: "submenu open")            }            self.openMenus[ObjectIdentifier(menu)] = menu            // Removed redundant async refresh - single pass is sufficient after initial layout            return        }         var provider: UsageProvider?        if self.shouldMergeIcons {            let resolvedProvider = self.resolvedMenuProvider()            self.lastMenuProvider = resolvedProvider ?? .codex            provider = resolvedProvider        } else {            if let menuProvider = self.menuProviders[ObjectIdentifier(menu)] {                self.lastMenuProvider = menuProvider                provider = menuProvider            } else if menu === self.fallbackMenu {                self.lastMenuProvider = self.store.enabledProviders().first ?? .codex                provider = nil            } else {                let resolved = self.store.enabledProviders().first ?? .codex                self.lastMenuProvider = resolved                provider = resolved            }        }         let didRefresh = self.menuNeedsRefresh(menu)        if didRefresh {            self.populateMenu(menu, provider: provider)            self.markMenuFresh(menu)            // Heights are already set during populateMenu, no need to remeasure        }        self.openMenus[ObjectIdentifier(menu)] = menu        // Only schedule refresh after menu is registered as open - refreshNow is called async        if Self.menuRefreshEnabled {            self.scheduleOpenMenuRefresh(for: menu)        }    }     func menuDidClose(_ menu: NSMenu) {        let key = ObjectIdentifier(menu)         self.openMenus.removeValue(forKey: key)        self.menuRefreshTasks.removeValue(forKey: key)?.cancel()         let isPersistentMenu = menu === self.mergedMenu ||            menu === self.fallbackMenu ||            self.providerMenus.values.contains { $0 === menu }        if !isPersistentMenu {            self.menuProviders.removeValue(forKey: key)            self.menuVersions.removeValue(forKey: key)        }        for menuItem in menu.items {            (menuItem.view as? MenuCardHighlighting)?.setHighlighted(false)        }    }     func menu(_ menu: NSMenu, willHighlight item: NSMenuItem?) {        for menuItem in menu.items {            let highlighted = menuItem == item && menuItem.isEnabled            (menuItem.view as? MenuCardHighlighting)?.setHighlighted(highlighted)        }    }     private func populateMenu(_ menu: NSMenu, provider: UsageProvider?) {        let selectedProvider = provider        let enabledProviders = self.store.enabledProviders()        let menuWidth = self.menuCardWidth(for: enabledProviders, menu: menu)        let currentProvider = selectedProvider ?? enabledProviders.first ?? .codex        let tokenAccountDisplay = self.tokenAccountMenuDisplay(for: currentProvider)        let showAllTokenAccounts = tokenAccountDisplay?.showAll ?? false        let openAIContext = self.openAIWebContext(            currentProvider: currentProvider,            showAllTokenAccounts: showAllTokenAccounts)         let hasTokenAccountSwitcher = menu.items.contains { $0.view is TokenAccountSwitcherView }        let switcherProvidersMatch = enabledProviders == self.lastSwitcherProviders        let switcherUsageBarsShowUsedMatch = self.settings.usageBarsShowUsed == self.lastSwitcherUsageBarsShowUsed        let canSmartUpdate = self.shouldMergeIcons &&            enabledProviders.count > 1 &&            switcherProvidersMatch &&            switcherUsageBarsShowUsedMatch &&            tokenAccountDisplay == nil &&            !hasTokenAccountSwitcher &&            !menu.items.isEmpty &&            menu.items.first?.view is ProviderSwitcherView         if canSmartUpdate {            self.updateMenuContent(                menu,                provider: selectedProvider,                currentProvider: currentProvider,                menuWidth: menuWidth,                openAIContext: openAIContext)            return        }         menu.removeAllItems()         let descriptor = MenuDescriptor.build(            provider: selectedProvider,            store: self.store,            settings: self.settings,            account: self.account,            updateReady: self.updater.updateStatus.isUpdateReady)         self.addProviderSwitcherIfNeeded(            to: menu,            enabledProviders: enabledProviders,            selectedProvider: selectedProvider)        // Track which providers the switcher was built with for smart update detection        if self.shouldMergeIcons, enabledProviders.count > 1 {            self.lastSwitcherProviders = enabledProviders            self.lastSwitcherUsageBarsShowUsed = self.settings.usageBarsShowUsed        }        self.addTokenAccountSwitcherIfNeeded(to: menu, display: tokenAccountDisplay)        let menuContext = MenuCardContext(            currentProvider: currentProvider,            selectedProvider: selectedProvider,            menuWidth: menuWidth,            tokenAccountDisplay: tokenAccountDisplay,            openAIContext: openAIContext)        let addedOpenAIWebItems = self.addMenuCards(to: menu, context: menuContext)        self.addOpenAIWebItemsIfNeeded(            to: menu,            currentProvider: currentProvider,            context: openAIContext,            addedOpenAIWebItems: addedOpenAIWebItems)        self.addActionableSections(descriptor.sections, to: menu)    }     /// Smart update: only rebuild content sections when switching providers (keep the switcher intact).    private func updateMenuContent(        _ menu: NSMenu,        provider: UsageProvider?,        currentProvider: UsageProvider,        menuWidth: CGFloat,        openAIContext: OpenAIWebContext)    {        // Batch menu updates to prevent visual flickering during provider switch.        CATransaction.begin()        CATransaction.setDisableActions(true)        defer { CATransaction.commit() }         var contentStartIndex = 0        if menu.items.first?.view is ProviderSwitcherView {            contentStartIndex = 2        }        if menu.items.count > contentStartIndex,           menu.items[contentStartIndex].view is TokenAccountSwitcherView        {            contentStartIndex += 2        }        while menu.items.count > contentStartIndex {            menu.removeItem(at: contentStartIndex)        }         let descriptor = MenuDescriptor.build(            provider: provider,            store: self.store,            settings: self.settings,            account: self.account,            updateReady: self.updater.updateStatus.isUpdateReady)         let menuContext = MenuCardContext(            currentProvider: currentProvider,            selectedProvider: provider,            menuWidth: menuWidth,            tokenAccountDisplay: nil,            openAIContext: openAIContext)        let addedOpenAIWebItems = self.addMenuCards(to: menu, context: menuContext)        self.addOpenAIWebItemsIfNeeded(            to: menu,            currentProvider: currentProvider,            context: openAIContext,            addedOpenAIWebItems: addedOpenAIWebItems)        self.addActionableSections(descriptor.sections, to: menu)    }     private struct OpenAIWebContext {        let hasUsageBreakdown: Bool        let hasCreditsHistory: Bool        let hasCostHistory: Bool        let hasOpenAIWebMenuItems: Bool    }     private struct MenuCardContext {        let currentProvider: UsageProvider        let selectedProvider: UsageProvider?        let menuWidth: CGFloat        let tokenAccountDisplay: TokenAccountMenuDisplay?        let openAIContext: OpenAIWebContext    }     private func openAIWebContext(        currentProvider: UsageProvider,        showAllTokenAccounts: Bool) -> OpenAIWebContext    {        let dashboard = self.store.openAIDashboard        let openAIWebEligible = currentProvider == .codex &&            self.store.openAIDashboardRequiresLogin == false &&            dashboard != nil        let hasCreditsHistory = openAIWebEligible && !(dashboard?.dailyBreakdown ?? []).isEmpty        let hasUsageBreakdown = openAIWebEligible && !(dashboard?.usageBreakdown ?? []).isEmpty        let hasCostHistory = self.settings.isCostUsageEffectivelyEnabled(for: currentProvider) &&            (self.store.tokenSnapshot(for: currentProvider)?.daily.isEmpty == false)        let hasOpenAIWebMenuItems = !showAllTokenAccounts &&            (hasCreditsHistory || hasUsageBreakdown || hasCostHistory)        return OpenAIWebContext(            hasUsageBreakdown: hasUsageBreakdown,            hasCreditsHistory: hasCreditsHistory,            hasCostHistory: hasCostHistory,            hasOpenAIWebMenuItems: hasOpenAIWebMenuItems)    }     private func addProviderSwitcherIfNeeded(        to menu: NSMenu,        enabledProviders: [UsageProvider],        selectedProvider: UsageProvider?)    {        guard self.shouldMergeIcons, enabledProviders.count > 1 else { return }        let switcherItem = self.makeProviderSwitcherItem(            providers: enabledProviders,            selected: selectedProvider,            menu: menu)        menu.addItem(switcherItem)        menu.addItem(.separator())    }     private func addTokenAccountSwitcherIfNeeded(to menu: NSMenu, display: TokenAccountMenuDisplay?) {        guard let display, display.showSwitcher else { return }        let switcherItem = self.makeTokenAccountSwitcherItem(display: display, menu: menu)        menu.addItem(switcherItem)        menu.addItem(.separator())    }     private func addMenuCards(to menu: NSMenu, context: MenuCardContext) -> Bool {        if let tokenAccountDisplay = context.tokenAccountDisplay, tokenAccountDisplay.showAll {            let accountSnapshots = tokenAccountDisplay.snapshots            let cards = accountSnapshots.isEmpty                ? []                : accountSnapshots.compactMap { accountSnapshot in                    self.menuCardModel(                        for: context.currentProvider,                        snapshotOverride: accountSnapshot.snapshot,                        errorOverride: accountSnapshot.error)                }            if cards.isEmpty, let model = self.menuCardModel(for: context.selectedProvider) {                menu.addItem(self.makeMenuCardItem(                    UsageMenuCardView(model: model, width: context.menuWidth),                    id: "menuCard",                    width: context.menuWidth))                menu.addItem(.separator())            } else {                for (index, model) in cards.enumerated() {                    menu.addItem(self.makeMenuCardItem(                        UsageMenuCardView(model: model, width: context.menuWidth),                        id: "menuCard-\(index)",                        width: context.menuWidth))                    if index < cards.count - 1 {                        menu.addItem(.separator())                    }                }                if !cards.isEmpty {                    menu.addItem(.separator())                }            }            return false        }         guard let model = self.menuCardModel(for: context.selectedProvider) else { return false }        if context.openAIContext.hasOpenAIWebMenuItems {            let webItems = OpenAIWebMenuItems(                hasUsageBreakdown: context.openAIContext.hasUsageBreakdown,                hasCreditsHistory: context.openAIContext.hasCreditsHistory,                hasCostHistory: context.openAIContext.hasCostHistory)            self.addMenuCardSections(                to: menu,                model: model,                provider: context.currentProvider,                width: context.menuWidth,                webItems: webItems)            return true        }         menu.addItem(self.makeMenuCardItem(            UsageMenuCardView(model: model, width: context.menuWidth),            id: "menuCard",            width: context.menuWidth))        if context.currentProvider == .codex, model.creditsText != nil {            menu.addItem(self.makeBuyCreditsItem())        }        menu.addItem(.separator())        return false    }     private func addOpenAIWebItemsIfNeeded(        to menu: NSMenu,        currentProvider: UsageProvider,        context: OpenAIWebContext,        addedOpenAIWebItems: Bool)    {        guard context.hasOpenAIWebMenuItems else { return }        if !addedOpenAIWebItems {            // Only show these when we actually have additional data.            if context.hasUsageBreakdown {                _ = self.addUsageBreakdownSubmenu(to: menu)            }            if context.hasCreditsHistory {                _ = self.addCreditsHistorySubmenu(to: menu)            }            if context.hasCostHistory {                _ = self.addCostHistorySubmenu(to: menu, provider: currentProvider)            }        }        menu.addItem(.separator())    }     private func addActionableSections(_ sections: [MenuDescriptor.Section], to menu: NSMenu) {        let actionableSections = sections.filter { section in            section.entries.contains { entry in                if case .action = entry { return true }                return false            }        }        for (index, section) in actionableSections.enumerated() {            for entry in section.entries {                switch entry {                case let .text(text, style):                    let item = NSMenuItem(title: text, action: nil, keyEquivalent: "")                    item.isEnabled = false                    if style == .headline {                        let font = NSFont.systemFont(ofSize: NSFont.systemFontSize, weight: .semibold)                        item.attributedTitle = NSAttributedString(string: text, attributes: [.font: font])                    } else if style == .secondary {                        let font = NSFont.systemFont(ofSize: NSFont.smallSystemFontSize)                        item.attributedTitle = NSAttributedString(                            string: text,                            attributes: [.font: font, .foregroundColor: NSColor.secondaryLabelColor])                    }                    menu.addItem(item)                case let .action(title, action):                    let (selector, represented) = self.selector(for: action)                    let item = NSMenuItem(title: title, action: selector, keyEquivalent: "")                    item.target = self                    item.representedObject = represented                    if let iconName = action.systemImageName,                       let image = NSImage(systemSymbolName: iconName, accessibilityDescription: nil)                    {                        image.isTemplate = true                        image.size = NSSize(width: 16, height: 16)                        item.image = image                    }                    if case let .switchAccount(targetProvider) = action,                       let subtitle = self.switchAccountSubtitle(for: targetProvider)                    {                        item.isEnabled = false                        self.applySubtitle(subtitle, to: item, title: title)                    }                    menu.addItem(item)                case .divider:                    menu.addItem(.separator())                }            }            if index < actionableSections.count - 1 {                menu.addItem(.separator())            }        }    }     func makeMenu(for provider: UsageProvider?) -> NSMenu {        let menu = NSMenu()        menu.autoenablesItems = false        menu.delegate = self        if let provider {            self.menuProviders[ObjectIdentifier(menu)] = provider        }        return menu    }     private func makeProviderSwitcherItem(        providers: [UsageProvider],        selected: UsageProvider?,        menu: NSMenu) -> NSMenuItem    {        let view = ProviderSwitcherView(            providers: providers,            selected: selected,            width: self.menuCardWidth(for: providers, menu: menu),            showsIcons: self.settings.switcherShowsIcons,            iconProvider: { [weak self] provider in                self?.switcherIcon(for: provider) ?? NSImage()            },            weeklyRemainingProvider: { [weak self] provider in                self?.switcherWeeklyRemaining(for: provider)            },            onSelect: { [weak self, weak menu] provider in                guard let self, let menu else { return }                self.selectedMenuProvider = provider                self.lastMenuProvider = provider                self.populateMenu(menu, provider: provider)                self.markMenuFresh(menu)                self.applyIcon(phase: nil)            })        let item = NSMenuItem()        item.view = view        item.isEnabled = false        return item    }     private func makeTokenAccountSwitcherItem(        display: TokenAccountMenuDisplay,        menu: NSMenu) -> NSMenuItem    {        let view = TokenAccountSwitcherView(            accounts: display.accounts,            selectedIndex: display.activeIndex,            width: self.menuCardWidth(for: self.store.enabledProviders(), menu: menu),            onSelect: { [weak self, weak menu] index in                guard let self, let menu else { return }                self.settings.setActiveTokenAccountIndex(index, for: display.provider)                Task { @MainActor in                    await ProviderInteractionContext.$current.withValue(.userInitiated) {                        await self.store.refresh()                    }                }                self.populateMenu(menu, provider: display.provider)                self.markMenuFresh(menu)                self.applyIcon(phase: nil)            })        let item = NSMenuItem()        item.view = view        item.isEnabled = false        return item    }     private func resolvedMenuProvider() -> UsageProvider? {        let enabled = self.store.enabledProviders()        if enabled.isEmpty { return .codex }        if let selected = self.selectedMenuProvider, enabled.contains(selected) {            return selected        }        return enabled.first    }     private func tokenAccountMenuDisplay(for provider: UsageProvider) -> TokenAccountMenuDisplay? {        guard TokenAccountSupportCatalog.support(for: provider) != nil else { return nil }        let accounts = self.settings.tokenAccounts(for: provider)        guard accounts.count > 1 else { return nil }        let activeIndex = self.settings.tokenAccountsData(for: provider)?.clampedActiveIndex() ?? 0        let showAll = self.settings.showAllTokenAccountsInMenu        let snapshots = showAll ? (self.store.accountSnapshots[provider] ?? []) : []        return TokenAccountMenuDisplay(            provider: provider,            accounts: accounts,

Lines 1–500 of 1266.