import AppKitimport CodexBarCoreimport KeyboardShortcutsimport Observationimport QuartzCoreimport Securityimport SwiftUI @mainstruct CodexBarApp: App { @NSApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate @State private var settings: SettingsStore @State private var store: UsageStore @State private var managedCodexAccountCoordinator: ManagedCodexAccountCoordinator @State private var codexAccountPromotionCoordinator: CodexAccountPromotionCoordinator private let preferencesSelection: PreferencesSelection private let account: AccountInfo init() { let env = ProcessInfo.processInfo.environment let storedLevel = CodexBarLog.parseLevel(UserDefaults.standard.string(forKey: "debugLogLevel")) ?? .verbose let level = CodexBarLog.parseLevel(env["CODEXBAR_LOG_LEVEL"]) ?? storedLevel CodexBarLog.bootstrapIfNeeded(.init( destination: .oslog(subsystem: "com.steipete.codexbar"), level: level, json: false)) let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "unknown" let build = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String ?? "unknown" let gitCommit = Bundle.main.object(forInfoDictionaryKey: "CodexGitCommit") as? String ?? "unknown" let buildTimestamp = Bundle.main.object(forInfoDictionaryKey: "CodexBuildTimestamp") as? String ?? "unknown" CodexBarLog.logger(LogCategories.app).info( "CodexBar starting", metadata: [ "version": version, "build": build, "git": gitCommit, "built": buildTimestamp, ]) KeychainAccessGate.isDisabled = UserDefaults.standard.bool(forKey: "debugDisableKeychainAccess") KeychainPromptCoordinator.install() let preferencesSelection = PreferencesSelection() let settings = SettingsStore() let managedCodexAccountCoordinator = ManagedCodexAccountCoordinator() managedCodexAccountCoordinator.onManagedAccountsDidChange = { _ = settings.persistResolvedCodexActiveSourceCorrectionIfNeeded() } _ = settings.persistResolvedCodexActiveSourceCorrectionIfNeeded() let fetcher = UsageFetcher() let browserDetection = BrowserDetection(cacheTTL: BrowserDetection.defaultCacheTTL) let account = fetcher.loadAccountInfo() let store = UsageStore(fetcher: fetcher, browserDetection: browserDetection, settings: settings) let codexAccountPromotionCoordinator = CodexAccountPromotionCoordinator( settingsStore: settings, usageStore: store, managedAccountCoordinator: managedCodexAccountCoordinator) self.preferencesSelection = preferencesSelection _settings = State(wrappedValue: settings) _store = State(wrappedValue: store) _managedCodexAccountCoordinator = State(wrappedValue: managedCodexAccountCoordinator) _codexAccountPromotionCoordinator = State(wrappedValue: codexAccountPromotionCoordinator) self.account = account CodexBarLog.setLogLevel(settings.debugLogLevel) self.appDelegate.configure(.init( store: store, settings: settings, account: account, selection: preferencesSelection, managedCodexAccountCoordinator: managedCodexAccountCoordinator, codexAccountPromotionCoordinator: codexAccountPromotionCoordinator)) } @SceneBuilder var body: some Scene { // Hidden 1×1 window to keep SwiftUI's lifecycle alive so `Settings` scene // shows the native toolbar tabs even though the UI is AppKit-based. WindowGroup("CodexBarLifecycleKeepalive") { HiddenWindowView() } .defaultSize(width: 20, height: 20) .windowStyle(.hiddenTitleBar) Settings { PreferencesView( settings: self.settings, store: self.store, updater: self.appDelegate.updaterController, selection: self.preferencesSelection, managedCodexAccountCoordinator: self.managedCodexAccountCoordinator, codexAccountPromotionCoordinator: self.codexAccountPromotionCoordinator) } .defaultSize(width: PreferencesTab.general.preferredWidth, height: PreferencesTab.general.preferredHeight) .windowResizability(.contentSize) } private func openSettings(tab: PreferencesTab) { self.preferencesSelection.tab = tab NSApp.activate(ignoringOtherApps: true) _ = NSApp.sendAction(Selector(("showPreferencesWindow:")), to: nil, from: nil) }} // MARK: - Updater abstraction @MainActorprotocol UpdaterProviding: AnyObject { var automaticallyChecksForUpdates: Bool { get set } var automaticallyDownloadsUpdates: Bool { get set } var isAvailable: Bool { get } var unavailableReason: String? { get } var updateStatus: UpdateStatus { get } func checkForUpdates(_ sender: Any?)} /// No-op updater used for debug builds and non-bundled runs to suppress Sparkle dialogs.final class DisabledUpdaterController: UpdaterProviding { var automaticallyChecksForUpdates: Bool = false var automaticallyDownloadsUpdates: Bool = false let isAvailable: Bool = false let unavailableReason: String? let updateStatus = UpdateStatus() init(unavailableReason: String? = nil) { self.unavailableReason = unavailableReason } func checkForUpdates(_ sender: Any?) {}} @MainActor@Observablefinal class UpdateStatus { static let disabled = UpdateStatus() var isUpdateReady: Bool init(isUpdateReady: Bool = false) { self.isUpdateReady = isUpdateReady }} #if canImport(Sparkle) && ENABLE_SPARKLEimport Sparkle @MainActorfinal class SparkleUpdaterController: NSObject, UpdaterProviding, SPUUpdaterDelegate { private lazy var controller = SPUStandardUpdaterController( startingUpdater: false, updaterDelegate: self, userDriverDelegate: nil) let updateStatus = UpdateStatus() let unavailableReason: String? = nil init(savedAutoUpdate: Bool) { super.init() let updater = self.controller.updater updater.automaticallyChecksForUpdates = savedAutoUpdate updater.automaticallyDownloadsUpdates = savedAutoUpdate self.controller.startUpdater() } var automaticallyChecksForUpdates: Bool { get { self.controller.updater.automaticallyChecksForUpdates } set { self.controller.updater.automaticallyChecksForUpdates = newValue } } var automaticallyDownloadsUpdates: Bool { get { self.controller.updater.automaticallyDownloadsUpdates } set { self.controller.updater.automaticallyDownloadsUpdates = newValue } } var isAvailable: Bool { true } func checkForUpdates(_ sender: Any?) { self.controller.checkForUpdates(sender) } nonisolated func updater(_ updater: SPUUpdater, didDownloadUpdate item: SUAppcastItem) { Task { @MainActor in self.updateStatus.isUpdateReady = true } } nonisolated func updater(_ updater: SPUUpdater, failedToDownloadUpdate item: SUAppcastItem, error: Error) { Task { @MainActor in self.updateStatus.isUpdateReady = false } } nonisolated func userDidCancelDownload(_ updater: SPUUpdater) { Task { @MainActor in self.updateStatus.isUpdateReady = false } } nonisolated func updater( _ updater: SPUUpdater, userDidMake choice: SPUUserUpdateChoice, forUpdate updateItem: SUAppcastItem, state: SPUUserUpdateState) { let downloaded = state.stage == .downloaded Task { @MainActor in switch choice { case .install, .skip: self.updateStatus.isUpdateReady = false case .dismiss: self.updateStatus.isUpdateReady = downloaded @unknown default: self.updateStatus.isUpdateReady = false } } } nonisolated func allowedChannels(for updater: SPUUpdater) -> Set<String> { UpdateChannel.current.allowedSparkleChannels }} private func isDeveloperIDSigned(bundleURL: URL) -> Bool { var staticCode: SecStaticCode? guard SecStaticCodeCreateWithPath(bundleURL as CFURL, SecCSFlags(), &staticCode) == errSecSuccess, let code = staticCode else { return false } var infoCF: CFDictionary? guard SecCodeCopySigningInformation(code, SecCSFlags(rawValue: kSecCSSigningInformation), &infoCF) == errSecSuccess, let info = infoCF as? [String: Any], let certs = info[kSecCodeInfoCertificates as String] as? [SecCertificate], let leaf = certs.first else { return false } if let summary = SecCertificateCopySubjectSummary(leaf) as String? { return summary.hasPrefix("Developer ID Application:") } return false} @MainActorprivate func makeUpdaterController() -> UpdaterProviding { let bundleURL = Bundle.main.bundleURL let isBundledApp = bundleURL.pathExtension == "app" guard isBundledApp else { return DisabledUpdaterController(unavailableReason: "Updates unavailable in this build.") } if InstallOrigin.isHomebrewCask(appBundleURL: bundleURL) { return DisabledUpdaterController( unavailableReason: "Updates managed by Homebrew. Run: brew upgrade --cask steipete/tap/codexbar") } guard isDeveloperIDSigned(bundleURL: bundleURL) else { return DisabledUpdaterController(unavailableReason: "Updates unavailable in this build.") } let defaults = UserDefaults.standard let autoUpdateKey = "autoUpdateEnabled" // Default to true for first launch; fall back to saved preference thereafter. let savedAutoUpdate = (defaults.object(forKey: autoUpdateKey) as? Bool) ?? true return SparkleUpdaterController(savedAutoUpdate: savedAutoUpdate)}#elseprivate func makeUpdaterController() -> UpdaterProviding { DisabledUpdaterController()}#endif @MainActorfinal class AppDelegate: NSObject, NSApplicationDelegate { struct Dependencies { let store: UsageStore let settings: SettingsStore let account: AccountInfo let selection: PreferencesSelection let managedCodexAccountCoordinator: ManagedCodexAccountCoordinator let codexAccountPromotionCoordinator: CodexAccountPromotionCoordinator } let updaterController: UpdaterProviding = makeUpdaterController() private var statusController: StatusItemControlling? private var store: UsageStore? private var settings: SettingsStore? private var account: AccountInfo? private var preferencesSelection: PreferencesSelection? private var managedCodexAccountCoordinator: ManagedCodexAccountCoordinator? private var codexAccountPromotionCoordinator: CodexAccountPromotionCoordinator? func configure(_ dependencies: Dependencies) { self.store = dependencies.store self.settings = dependencies.settings self.account = dependencies.account self.preferencesSelection = dependencies.selection self.managedCodexAccountCoordinator = dependencies.managedCodexAccountCoordinator self.codexAccountPromotionCoordinator = dependencies.codexAccountPromotionCoordinator } func applicationWillFinishLaunching(_ notification: Notification) { self.configureAppIconForMacOSVersion() } func applicationDidFinishLaunching(_ notification: Notification) { AppNotifications.shared.requestAuthorizationOnStartup() self.ensureStatusController() KeyboardShortcuts.onKeyUp(for: .openMenu) { [weak self] in Task { @MainActor [weak self] in self?.statusController?.openMenuFromShortcut() } } } func applicationWillTerminate(_ notification: Notification) { TTYCommandRunner.terminateActiveProcessesForAppShutdown() } /// Use the classic (non-Liquid Glass) app icon on macOS versions before 26. private func configureAppIconForMacOSVersion() { if #unavailable(macOS 26) { self.applyClassicAppIcon() } } private func applyClassicAppIcon() { guard let classicIcon = Self.loadClassicIcon() else { return } NSApp.applicationIconImage = classicIcon } private static func loadClassicIcon() -> NSImage? { guard let url = self.classicIconURL(), let image = NSImage(contentsOf: url) else { return nil } return image } private static func classicIconURL() -> URL? { Bundle.main.url(forResource: "Icon-classic", withExtension: "icns") } private func ensureStatusController() { if self.statusController != nil { return } if let store, let settings, let account, let selection = self.preferencesSelection, let managedCodexAccountCoordinator, let codexAccountPromotionCoordinator { self.statusController = StatusItemController.factory( store, settings, account, self.updaterController, selection, managedCodexAccountCoordinator, codexAccountPromotionCoordinator) return } // Defensive fallback: this should not be hit in normal app lifecycle. CodexBarLog.logger(LogCategories.app) .error("StatusItemController fallback path used; settings/store mismatch likely.") assertionFailure("StatusItemController fallback path used; check app lifecycle wiring.") let fallbackSettings = SettingsStore() let fetcher = UsageFetcher() let browserDetection = BrowserDetection(cacheTTL: BrowserDetection.defaultCacheTTL) let fallbackAccount = fetcher.loadAccountInfo() let fallbackStore = UsageStore(fetcher: fetcher, browserDetection: browserDetection, settings: fallbackSettings) let fallbackManagedCodexAccountCoordinator = ManagedCodexAccountCoordinator() let fallbackCodexAccountPromotionCoordinator = CodexAccountPromotionCoordinator( settingsStore: fallbackSettings, usageStore: fallbackStore, managedAccountCoordinator: fallbackManagedCodexAccountCoordinator) self.statusController = StatusItemController.factory( fallbackStore, fallbackSettings, fallbackAccount, self.updaterController, PreferencesSelection(), fallbackManagedCodexAccountCoordinator, fallbackCodexAccountPromotionCoordinator) }}
import AppKit
import CodexBarCore
import KeyboardShortcuts
import Observation
import QuartzCore
import Security
import SwiftUI
@main
struct CodexBarApp: App {
@NSApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
@State private var settings: SettingsStore
@State private var store: UsageStore
@State private var managedCodexAccountCoordinator: ManagedCodexAccountCoordinator
@State private var codexAccountPromotionCoordinator: CodexAccountPromotionCoordinator
private let preferencesSelection: PreferencesSelection
private let account: AccountInfo
init() {
let env = ProcessInfo.processInfo.environment
let storedLevel = CodexBarLog.parseLevel(UserDefaults.standard.string(forKey: "debugLogLevel")) ?? .verbose
let level = CodexBarLog.parseLevel(env["CODEXBAR_LOG_LEVEL"]) ?? storedLevel
CodexBarLog.bootstrapIfNeeded(.init(
destination: .oslog(subsystem: "com.steipete.codexbar"),
level: level,
json: false))
let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "unknown"
let build = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String ?? "unknown"
let gitCommit = Bundle.main.object(forInfoDictionaryKey: "CodexGitCommit") as? String ?? "unknown"
let buildTimestamp = Bundle.main.object(forInfoDictionaryKey: "CodexBuildTimestamp") as? String ?? "unknown"
CodexBarLog.logger(LogCategories.app).info(
"CodexBar starting",
metadata: [
"version": version,
"build": build,
"git": gitCommit,
"built": buildTimestamp,
])
KeychainAccessGate.isDisabled = UserDefaults.standard.bool(forKey: "debugDisableKeychainAccess")
KeychainPromptCoordinator.install()
let preferencesSelection = PreferencesSelection()
let settings = SettingsStore()
let managedCodexAccountCoordinator = ManagedCodexAccountCoordinator()
managedCodexAccountCoordinator.onManagedAccountsDidChange = {
_ = settings.persistResolvedCodexActiveSourceCorrectionIfNeeded()
}
_ = settings.persistResolvedCodexActiveSourceCorrectionIfNeeded()
let fetcher = UsageFetcher()
let browserDetection = BrowserDetection(cacheTTL: BrowserDetection.defaultCacheTTL)
let account = fetcher.loadAccountInfo()
let store = UsageStore(fetcher: fetcher, browserDetection: browserDetection, settings: settings)
let codexAccountPromotionCoordinator = CodexAccountPromotionCoordinator(
settingsStore: settings,
usageStore: store,
managedAccountCoordinator: managedCodexAccountCoordinator)
self.preferencesSelection = preferencesSelection
_settings = State(wrappedValue: settings)
_store = State(wrappedValue: store)
_managedCodexAccountCoordinator = State(wrappedValue: managedCodexAccountCoordinator)
_codexAccountPromotionCoordinator = State(wrappedValue: codexAccountPromotionCoordinator)
self.account = account
CodexBarLog.setLogLevel(settings.debugLogLevel)
self.appDelegate.configure(.init(
store: store,
settings: settings,
account: account,
selection: preferencesSelection,
managedCodexAccountCoordinator: managedCodexAccountCoordinator,
codexAccountPromotionCoordinator: codexAccountPromotionCoordinator))
}
@SceneBuilder
var body: some Scene {
// Hidden 1×1 window to keep SwiftUI's lifecycle alive so `Settings` scene
// shows the native toolbar tabs even though the UI is AppKit-based.
WindowGroup("CodexBarLifecycleKeepalive") {
HiddenWindowView()
}
.defaultSize(width: 20, height: 20)
.windowStyle(.hiddenTitleBar)
Settings {
PreferencesView(
settings: self.settings,
store: self.store,
updater: self.appDelegate.updaterController,
selection: self.preferencesSelection,
managedCodexAccountCoordinator: self.managedCodexAccountCoordinator,
codexAccountPromotionCoordinator: self.codexAccountPromotionCoordinator)
}
.defaultSize(width: PreferencesTab.general.preferredWidth, height: PreferencesTab.general.preferredHeight)
.windowResizability(.contentSize)
}
private func openSettings(tab: PreferencesTab) {
self.preferencesSelection.tab = tab
NSApp.activate(ignoringOtherApps: true)
_ = NSApp.sendAction(Selector(("showPreferencesWindow:")), to: nil, from: nil)
}
}
// MARK: - Updater abstraction
@MainActor
protocol UpdaterProviding: AnyObject {
var automaticallyChecksForUpdates: Bool { get set }
var automaticallyDownloadsUpdates: Bool { get set }
var isAvailable: Bool { get }
var unavailableReason: String? { get }
var updateStatus: UpdateStatus { get }
func checkForUpdates(_ sender: Any?)
}
/// No-op updater used for debug builds and non-bundled runs to suppress Sparkle dialogs.
final class DisabledUpdaterController: UpdaterProviding {
var automaticallyChecksForUpdates: Bool = false
var automaticallyDownloadsUpdates: Bool = false
let isAvailable: Bool = false
let unavailableReason: String?
let updateStatus = UpdateStatus()
init(unavailableReason: String? = nil) {
self.unavailableReason = unavailableReason
}
func checkForUpdates(_ sender: Any?) {}
}
@MainActor
@Observable
final class UpdateStatus {
static let disabled = UpdateStatus()
var isUpdateReady: Bool
init(isUpdateReady: Bool = false) {
self.isUpdateReady = isUpdateReady
}
}
#if canImport(Sparkle) && ENABLE_SPARKLE
import Sparkle
@MainActor
final class SparkleUpdaterController: NSObject, UpdaterProviding, SPUUpdaterDelegate {
private lazy var controller = SPUStandardUpdaterController(
startingUpdater: false,
updaterDelegate: self,
userDriverDelegate: nil)
let updateStatus = UpdateStatus()
let unavailableReason: String? = nil
init(savedAutoUpdate: Bool) {
super.init()
let updater = self.controller.updater
updater.automaticallyChecksForUpdates = savedAutoUpdate
updater.automaticallyDownloadsUpdates = savedAutoUpdate
self.controller.startUpdater()
}
var automaticallyChecksForUpdates: Bool {
get { self.controller.updater.automaticallyChecksForUpdates }
set { self.controller.updater.automaticallyChecksForUpdates = newValue }
}
var automaticallyDownloadsUpdates: Bool {
get { self.controller.updater.automaticallyDownloadsUpdates }
set { self.controller.updater.automaticallyDownloadsUpdates = newValue }
}
var isAvailable: Bool {
true
}
func checkForUpdates(_ sender: Any?) {
self.controller.checkForUpdates(sender)
}
nonisolated func updater(_ updater: SPUUpdater, didDownloadUpdate item: SUAppcastItem) {
Task { @MainActor in
self.updateStatus.isUpdateReady = true
}
}
nonisolated func updater(_ updater: SPUUpdater, failedToDownloadUpdate item: SUAppcastItem, error: Error) {
Task { @MainActor in
self.updateStatus.isUpdateReady = false
}
}
nonisolated func userDidCancelDownload(_ updater: SPUUpdater) {
Task { @MainActor in
self.updateStatus.isUpdateReady = false
}
}
nonisolated func updater(
_ updater: SPUUpdater,
userDidMake choice: SPUUserUpdateChoice,
forUpdate updateItem: SUAppcastItem,
state: SPUUserUpdateState)
{
let downloaded = state.stage == .downloaded
Task { @MainActor in
switch choice {
case .install, .skip:
self.updateStatus.isUpdateReady = false
case .dismiss:
self.updateStatus.isUpdateReady = downloaded
@unknown default:
self.updateStatus.isUpdateReady = false
}
}
}
nonisolated func allowedChannels(for updater: SPUUpdater) -> Set<String> {
UpdateChannel.current.allowedSparkleChannels
}
}
private func isDeveloperIDSigned(bundleURL: URL) -> Bool {
var staticCode: SecStaticCode?
guard SecStaticCodeCreateWithPath(bundleURL as CFURL, SecCSFlags(), &staticCode) == errSecSuccess,
let code = staticCode else { return false }
var infoCF: CFDictionary?
guard SecCodeCopySigningInformation(code, SecCSFlags(rawValue: kSecCSSigningInformation), &infoCF) == errSecSuccess,
let info = infoCF as? [String: Any],
let certs = info[kSecCodeInfoCertificates as String] as? [SecCertificate],
let leaf = certs.first else { return false }
if let summary = SecCertificateCopySubjectSummary(leaf) as String? {
return summary.hasPrefix("Developer ID Application:")
}
return false
}
@MainActor
private func makeUpdaterController() -> UpdaterProviding {
let bundleURL = Bundle.main.bundleURL
let isBundledApp = bundleURL.pathExtension == "app"
guard isBundledApp else {
return DisabledUpdaterController(unavailableReason: "Updates unavailable in this build.")
}
if InstallOrigin.isHomebrewCask(appBundleURL: bundleURL) {
return DisabledUpdaterController(
unavailableReason: "Updates managed by Homebrew. Run: brew upgrade --cask steipete/tap/codexbar")
}
guard isDeveloperIDSigned(bundleURL: bundleURL) else {
return DisabledUpdaterController(unavailableReason: "Updates unavailable in this build.")
}
let defaults = UserDefaults.standard
let autoUpdateKey = "autoUpdateEnabled"
// Default to true for first launch; fall back to saved preference thereafter.
let savedAutoUpdate = (defaults.object(forKey: autoUpdateKey) as? Bool) ?? true
return SparkleUpdaterController(savedAutoUpdate: savedAutoUpdate)
}
#else
private func makeUpdaterController() -> UpdaterProviding {
DisabledUpdaterController()
}
#endif
@MainActor
final class AppDelegate: NSObject, NSApplicationDelegate {
struct Dependencies {
let store: UsageStore
let settings: SettingsStore
let account: AccountInfo
let selection: PreferencesSelection
let managedCodexAccountCoordinator: ManagedCodexAccountCoordinator
let codexAccountPromotionCoordinator: CodexAccountPromotionCoordinator
}
let updaterController: UpdaterProviding = makeUpdaterController()
private var statusController: StatusItemControlling?
private var store: UsageStore?
private var settings: SettingsStore?
private var account: AccountInfo?
private var preferencesSelection: PreferencesSelection?
private var managedCodexAccountCoordinator: ManagedCodexAccountCoordinator?
private var codexAccountPromotionCoordinator: CodexAccountPromotionCoordinator?
func configure(_ dependencies: Dependencies) {
self.store = dependencies.store
self.settings = dependencies.settings
self.account = dependencies.account
self.preferencesSelection = dependencies.selection
self.managedCodexAccountCoordinator = dependencies.managedCodexAccountCoordinator
self.codexAccountPromotionCoordinator = dependencies.codexAccountPromotionCoordinator
}
func applicationWillFinishLaunching(_ notification: Notification) {
self.configureAppIconForMacOSVersion()
}
func applicationDidFinishLaunching(_ notification: Notification) {
AppNotifications.shared.requestAuthorizationOnStartup()
self.ensureStatusController()
KeyboardShortcuts.onKeyUp(for: .openMenu) { [weak self] in
Task { @MainActor [weak self] in
self?.statusController?.openMenuFromShortcut()
}
}
}
func applicationWillTerminate(_ notification: Notification) {
TTYCommandRunner.terminateActiveProcessesForAppShutdown()
}
/// Use the classic (non-Liquid Glass) app icon on macOS versions before 26.
private func configureAppIconForMacOSVersion() {
if #unavailable(macOS 26) {
self.applyClassicAppIcon()
}
}
private func applyClassicAppIcon() {
guard let classicIcon = Self.loadClassicIcon() else { return }
NSApp.applicationIconImage = classicIcon
}
private static func loadClassicIcon() -> NSImage? {
guard let url = self.classicIconURL(),
let image = NSImage(contentsOf: url)
else {
return nil
}
return image
}
private static func classicIconURL() -> URL? {
Bundle.main.url(forResource: "Icon-classic", withExtension: "icns")
}
private func ensureStatusController() {
if self.statusController != nil { return }
if let store,
let settings,
let account,
let selection = self.preferencesSelection,
let managedCodexAccountCoordinator,
let codexAccountPromotionCoordinator
{
self.statusController = StatusItemController.factory(
store,
settings,
account,
self.updaterController,
selection,
managedCodexAccountCoordinator,
codexAccountPromotionCoordinator)
return
}
// Defensive fallback: this should not be hit in normal app lifecycle.
CodexBarLog.logger(LogCategories.app)
.error("StatusItemController fallback path used; settings/store mismatch likely.")
assertionFailure("StatusItemController fallback path used; check app lifecycle wiring.")
let fallbackSettings = SettingsStore()
let fetcher = UsageFetcher()
let browserDetection = BrowserDetection(cacheTTL: BrowserDetection.defaultCacheTTL)
let fallbackAccount = fetcher.loadAccountInfo()
let fallbackStore = UsageStore(fetcher: fetcher, browserDetection: browserDetection, settings: fallbackSettings)
let fallbackManagedCodexAccountCoordinator = ManagedCodexAccountCoordinator()
let fallbackCodexAccountPromotionCoordinator = CodexAccountPromotionCoordinator(
settingsStore: fallbackSettings,
usageStore: fallbackStore,
managedAccountCoordinator: fallbackManagedCodexAccountCoordinator)
self.statusController = StatusItemController.factory(
fallbackStore,
fallbackSettings,
fallbackAccount,
self.updaterController,
PreferencesSelection(),
fallbackManagedCodexAccountCoordinator,
fallbackCodexAccountPromotionCoordinator)
}
}