import AppKitimport CodexBarCoreimport SwiftUI extension StatusItemController { /// Smart update: rebuild everything below the provider switcher while keeping the switcher view intact. struct MenuUpdateContext { let provider: UsageProvider? let currentProvider: UsageProvider let switcherSelection: ProviderSwitcherSelection let menuWidth: CGFloat let codexAccountDisplay: CodexAccountMenuDisplay? let tokenAccountDisplay: TokenAccountMenuDisplay? let openAIContext: OpenAIWebContext let descriptor: MenuDescriptor } /// Smart update: rebuild everything below the provider switcher while keeping the switcher view intact. func updateMenuContentPreservingSwitcher( _ menu: NSMenu, context: MenuUpdateContext) { self.performMenuMutationWithoutAnimation { let contentStartIndex = self.providerSwitcherContentStartIndex(in: menu) if let switcherView = menu.items.first?.view as? ProviderSwitcherView { switcherView.updateSelection(context.switcherSelection) switcherView.updateQuotaIndicators() } let outgoingSelection = self.lastMergedMenuContentSelection let isSelectionSwitch = outgoingSelection != nil && outgoingSelection != context.switcherSelection let enabledProviders = self.store.enabledProvidersForDisplay() if isSelectionSwitch, let outgoingSelection, let cachedItems = self.reusableMergedSwitcherContent( for: context.switcherSelection, in: menu, menuWidth: context.menuWidth, codexAccountDisplay: context.codexAccountDisplay, tokenAccountDisplay: context.tokenAccountDisplay) { // Park the outgoing payloads for an equally instant switch-back. Compatible // menu-item shells stay attached, avoiding the empty intermediate layout that // AppKit can visibly render when the whole content block is removed first. let outgoingCodexAccountDisplay = self.lastCodexAccountMenuDisplay let outgoingTokenAccountDisplay = self.lastTokenAccountMenuDisplay self.rememberMergedSwitcherState(enabledProviders, context.switcherSelection) let displacedItems = self.replaceMenuContentKeepingRowsVisible( menu, fromIndex: contentStartIndex, with: cachedItems) self.cacheMergedSwitcherContent( displacedItems, in: menu, selection: outgoingSelection, context: MergedSwitcherContentCacheContext( menuWidth: context.menuWidth, codexAccountDisplay: outgoingCodexAccountDisplay, tokenAccountDisplay: outgoingTokenAccountDisplay, contentVersion: nil)) self.lastCodexAccountMenuDisplay = context.codexAccountDisplay self.lastTokenAccountMenuDisplay = context.tokenAccountDisplay self.cacheVisibleMergedSwitcherContent( in: menu, selection: context.switcherSelection, contentStartIndex: contentStartIndex, menuWidth: context.menuWidth, contentVersion: self.menuSession.contentVersion) return } // Rebuild path (data tick, or switch whose incoming tab must be built): recycle // the outgoing hosting views and reconcile in place when the row skeleton is // unchanged, so an open tracked menu sees content mutations instead of item // churn. The fresh content is built into a detached scratch menu while its // interaction closures capture the live menu they will serve. let shapes = self.menuContentShapes(in: menu, fromIndex: contentStartIndex) self.harvestRecyclableMenuCardViews( in: menu, fromIndex: contentStartIndex, displacedSelection: outgoingSelection, preserveHighlightedItem: true) defer { self.clearMenuCardViewRecyclePool() } self.rememberMergedSwitcherState(enabledProviders, context.switcherSelection) let scratch = NSMenu() scratch.autoenablesItems = false self.addSwitcherScopedMenuContent(into: scratch, captureMenu: menu, context: context) self.reconcileMenuContent(menu, fromIndex: contentStartIndex, shapes: shapes, with: scratch) self.cacheVisibleMergedSwitcherContent( in: menu, selection: context.switcherSelection, contentStartIndex: contentStartIndex, menuWidth: context.menuWidth, contentVersion: self.menuSession.contentVersion) } } /// Adds everything below the provider switcher (account switchers, card content, and /// actionable sections) to `target`, which may be a detached scratch menu; interaction /// closures always capture `captureMenu`, the live menu the rows will serve. private func addSwitcherScopedMenuContent( into target: NSMenu, captureMenu: NSMenu, context: MenuUpdateContext) { self.addCodexAccountSwitcherIfNeeded( to: target, display: context.codexAccountDisplay, width: context.menuWidth, captureMenu: captureMenu) self.lastCodexAccountMenuDisplay = context.codexAccountDisplay self.addTokenAccountSwitcherIfNeeded( to: target, display: context.tokenAccountDisplay, width: context.menuWidth, captureMenu: captureMenu) self.lastTokenAccountMenuDisplay = context.tokenAccountDisplay let menuContext = MenuCardContext( currentProvider: context.currentProvider, selectedProvider: context.provider, menuWidth: context.menuWidth, codexAccountDisplay: context.codexAccountDisplay, tokenAccountDisplay: context.tokenAccountDisplay, openAIContext: context.openAIContext) self.addPrimaryMenuContent( to: target, context: menuContext, switcherSelection: context.switcherSelection, captureMenu: captureMenu) self.addActionableSections( context.descriptor.sections, to: target, width: context.menuWidth, captureMenu: captureMenu) }}
import AppKit
import CodexBarCore
import SwiftUI
extension StatusItemController {
/// Smart update: rebuild everything below the provider switcher while keeping the switcher view intact.
struct MenuUpdateContext {
let provider: UsageProvider?
let currentProvider: UsageProvider
let switcherSelection: ProviderSwitcherSelection
let menuWidth: CGFloat
let codexAccountDisplay: CodexAccountMenuDisplay?
let tokenAccountDisplay: TokenAccountMenuDisplay?
let openAIContext: OpenAIWebContext
let descriptor: MenuDescriptor
}
/// Smart update: rebuild everything below the provider switcher while keeping the switcher view intact.
func updateMenuContentPreservingSwitcher(
_ menu: NSMenu,
context: MenuUpdateContext)
{
self.performMenuMutationWithoutAnimation {
let contentStartIndex = self.providerSwitcherContentStartIndex(in: menu)
if let switcherView = menu.items.first?.view as? ProviderSwitcherView {
switcherView.updateSelection(context.switcherSelection)
switcherView.updateQuotaIndicators()
}
let outgoingSelection = self.lastMergedMenuContentSelection
let isSelectionSwitch = outgoingSelection != nil && outgoingSelection != context.switcherSelection
let enabledProviders = self.store.enabledProvidersForDisplay()
if isSelectionSwitch,
let outgoingSelection,
let cachedItems = self.reusableMergedSwitcherContent(
for: context.switcherSelection,
in: menu,
menuWidth: context.menuWidth,
codexAccountDisplay: context.codexAccountDisplay,
tokenAccountDisplay: context.tokenAccountDisplay)
{
// Park the outgoing payloads for an equally instant switch-back. Compatible
// menu-item shells stay attached, avoiding the empty intermediate layout that
// AppKit can visibly render when the whole content block is removed first.
let outgoingCodexAccountDisplay = self.lastCodexAccountMenuDisplay
let outgoingTokenAccountDisplay = self.lastTokenAccountMenuDisplay
self.rememberMergedSwitcherState(enabledProviders, context.switcherSelection)
let displacedItems = self.replaceMenuContentKeepingRowsVisible(
menu,
fromIndex: contentStartIndex,
with: cachedItems)
self.cacheMergedSwitcherContent(
displacedItems,
in: menu,
selection: outgoingSelection,
context: MergedSwitcherContentCacheContext(
menuWidth: context.menuWidth,
codexAccountDisplay: outgoingCodexAccountDisplay,
tokenAccountDisplay: outgoingTokenAccountDisplay,
contentVersion: nil))
self.lastCodexAccountMenuDisplay = context.codexAccountDisplay
self.lastTokenAccountMenuDisplay = context.tokenAccountDisplay
self.cacheVisibleMergedSwitcherContent(
in: menu,
selection: context.switcherSelection,
contentStartIndex: contentStartIndex,
menuWidth: context.menuWidth,
contentVersion: self.menuSession.contentVersion)
return
}
// Rebuild path (data tick, or switch whose incoming tab must be built): recycle
// the outgoing hosting views and reconcile in place when the row skeleton is
// unchanged, so an open tracked menu sees content mutations instead of item
// churn. The fresh content is built into a detached scratch menu while its
// interaction closures capture the live menu they will serve.
let shapes = self.menuContentShapes(in: menu, fromIndex: contentStartIndex)
self.harvestRecyclableMenuCardViews(
in: menu,
fromIndex: contentStartIndex,
displacedSelection: outgoingSelection,
preserveHighlightedItem: true)
defer { self.clearMenuCardViewRecyclePool() }
self.rememberMergedSwitcherState(enabledProviders, context.switcherSelection)
let scratch = NSMenu()
scratch.autoenablesItems = false
self.addSwitcherScopedMenuContent(into: scratch, captureMenu: menu, context: context)
self.reconcileMenuContent(menu, fromIndex: contentStartIndex, shapes: shapes, with: scratch)
self.cacheVisibleMergedSwitcherContent(
in: menu,
selection: context.switcherSelection,
contentStartIndex: contentStartIndex,
menuWidth: context.menuWidth,
contentVersion: self.menuSession.contentVersion)
}
}
/// Adds everything below the provider switcher (account switchers, card content, and
/// actionable sections) to `target`, which may be a detached scratch menu; interaction
/// closures always capture `captureMenu`, the live menu the rows will serve.
private func addSwitcherScopedMenuContent(
into target: NSMenu,
captureMenu: NSMenu,
context: MenuUpdateContext)
{
self.addCodexAccountSwitcherIfNeeded(
to: target,
display: context.codexAccountDisplay,
width: context.menuWidth,
captureMenu: captureMenu)
self.lastCodexAccountMenuDisplay = context.codexAccountDisplay
self.addTokenAccountSwitcherIfNeeded(
to: target,
display: context.tokenAccountDisplay,
width: context.menuWidth,
captureMenu: captureMenu)
self.lastTokenAccountMenuDisplay = context.tokenAccountDisplay
let menuContext = MenuCardContext(
currentProvider: context.currentProvider,
selectedProvider: context.provider,
menuWidth: context.menuWidth,
codexAccountDisplay: context.codexAccountDisplay,
tokenAccountDisplay: context.tokenAccountDisplay,
openAIContext: context.openAIContext)
self.addPrimaryMenuContent(
to: target,
context: menuContext,
switcherSelection: context.switcherSelection,
captureMenu: captureMenu)
self.addActionableSections(
context.descriptor.sections,
to: target,
width: context.menuWidth,
captureMenu: captureMenu)
}
}