chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,881 @@
|
||||
import AppKit
|
||||
import CodexBarCore
|
||||
|
||||
extension StatusItemController {
|
||||
/// Identifies which manual refresh a task belongs to, so per-provider refreshes stay independent
|
||||
/// of each other and of the all-providers refresh.
|
||||
enum ManualRefreshScope: Hashable {
|
||||
case global
|
||||
case provider(UsageProvider)
|
||||
}
|
||||
}
|
||||
|
||||
enum LoginNotificationLogic {
|
||||
static func notificationCopy(providerName: String) -> (title: String, body: String) {
|
||||
(
|
||||
L("login_success_notification_title", providerName),
|
||||
L("login_success_notification_body"))
|
||||
}
|
||||
}
|
||||
|
||||
extension StatusItemController: StatusItemMenuPersistentActionDelegate {
|
||||
// MARK: - Actions reachable from menus
|
||||
|
||||
func refreshStore(
|
||||
forceTokenUsage: Bool,
|
||||
refreshOpenMenusWhenComplete: Bool = true,
|
||||
interaction: ProviderInteraction = .userInitiated)
|
||||
{
|
||||
Task {
|
||||
await self.performStoreRefresh(
|
||||
forceTokenUsage: forceTokenUsage,
|
||||
refreshOpenMenusWhenComplete: refreshOpenMenusWhenComplete,
|
||||
interaction: interaction)
|
||||
}
|
||||
}
|
||||
|
||||
func performStoreRefresh(
|
||||
forceTokenUsage: Bool,
|
||||
refreshOpenMenusWhenComplete: Bool,
|
||||
interaction: ProviderInteraction) async
|
||||
{
|
||||
await self.withProviderInteraction(interaction) {
|
||||
await self.store.refresh(forceTokenUsage: forceTokenUsage)
|
||||
guard !Task.isCancelled, !self.hasPreparedForAppShutdown else { return }
|
||||
self.store.scheduleStorageFootprintRefreshForOverview(force: true)
|
||||
if refreshOpenMenusWhenComplete {
|
||||
self.refreshOpenMenusAfterExplicitStoreAction()
|
||||
} else {
|
||||
self.invalidateMenus()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func performStoreRefresh(
|
||||
enrichmentMode: UsageStore.RefreshEnrichmentMode,
|
||||
refreshOpenMenusWhenComplete: Bool,
|
||||
interaction: ProviderInteraction) async
|
||||
{
|
||||
await self.withProviderInteraction(interaction) {
|
||||
await self.store.refresh(enrichmentMode: enrichmentMode)
|
||||
guard !Task.isCancelled, !self.hasPreparedForAppShutdown else { return }
|
||||
self.store.scheduleStorageFootprintRefreshForOverview(force: true)
|
||||
if refreshOpenMenusWhenComplete {
|
||||
self.refreshOpenMenusAfterExplicitStoreAction()
|
||||
} else {
|
||||
self.invalidateMenus()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func performStoreRefresh(
|
||||
for provider: UsageProvider,
|
||||
refreshOpenMenusWhenComplete: Bool,
|
||||
interaction: ProviderInteraction) async
|
||||
{
|
||||
await self.withProviderInteraction(interaction) {
|
||||
await self.store.awaitForcedRefreshEnrichment()
|
||||
guard !Task.isCancelled, !self.hasPreparedForAppShutdown else { return }
|
||||
let refreshStartedAt = Date()
|
||||
await self.store.refreshProvider(provider)
|
||||
guard !Task.isCancelled, !self.hasPreparedForAppShutdown else { return }
|
||||
await self.store.refreshProviderStatus(provider)
|
||||
guard !Task.isCancelled, !self.hasPreparedForAppShutdown else { return }
|
||||
await self.store.refreshTokenUsageNow(for: provider, force: true)
|
||||
guard !Task.isCancelled, !self.hasPreparedForAppShutdown else { return }
|
||||
if provider == .codex {
|
||||
await self.store.refreshCreditsNow(minimumSnapshotUpdatedAt: refreshStartedAt)
|
||||
guard !Task.isCancelled, !self.hasPreparedForAppShutdown else { return }
|
||||
await self.store.refreshOpenAIDashboardIfNeeded(
|
||||
force: true,
|
||||
expectedGuard: self.store.freshCodexOpenAIWebRefreshGuard(),
|
||||
bypassCoalescing: true)
|
||||
guard !Task.isCancelled, !self.hasPreparedForAppShutdown else { return }
|
||||
if self.store.openAIDashboardRequiresLogin {
|
||||
await self.store.refreshProvider(.codex)
|
||||
guard !Task.isCancelled, !self.hasPreparedForAppShutdown else { return }
|
||||
await self.store.refreshCreditsNow(minimumSnapshotUpdatedAt: refreshStartedAt)
|
||||
guard !Task.isCancelled, !self.hasPreparedForAppShutdown else { return }
|
||||
}
|
||||
}
|
||||
self.store.scheduleStorageFootprintRefresh(for: [provider], force: true)
|
||||
self.store.persistWidgetSnapshot(reason: "provider-refresh")
|
||||
if refreshOpenMenusWhenComplete {
|
||||
self.refreshOpenMenusAfterExplicitStoreAction()
|
||||
} else {
|
||||
self.invalidateMenus()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func withProviderInteraction(
|
||||
_ interaction: ProviderInteraction,
|
||||
operation: () async -> Void) async
|
||||
{
|
||||
if interaction == .userInitiated {
|
||||
await BrowserCookieAccessGate.withExplicitRetry {
|
||||
await ProviderInteractionContext.$current.withValue(interaction) {
|
||||
await operation()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await ProviderInteractionContext.$current.withValue(interaction) {
|
||||
await operation()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func refreshOpenMenusAfterExplicitStoreAction() {
|
||||
self.invalidateMenus(
|
||||
refreshOpenMenus: true,
|
||||
deferOpenParentMenuRebuild: true)
|
||||
}
|
||||
|
||||
@objc func refreshNow() {
|
||||
self.startManualRefresh(
|
||||
for: nil,
|
||||
originatingMenuID: nil,
|
||||
originatingMenuInteractionGeneration: nil)
|
||||
}
|
||||
|
||||
@objc func refreshMenuItem(_ sender: NSMenuItem) {
|
||||
self.refreshMenuProviderNow(in: sender.menu)
|
||||
}
|
||||
|
||||
func refreshMenuProviderNow(in menu: NSMenu?) {
|
||||
let originatingMenuID = menu.map(ObjectIdentifier.init)
|
||||
let originatingMenuInteractionGeneration = originatingMenuID.flatMap {
|
||||
self.menuSession.menuInteractionGeneration(for: $0)
|
||||
}
|
||||
self.startManualRefresh(
|
||||
for: self.manualRefreshProvider(for: menu),
|
||||
originatingMenuID: originatingMenuID,
|
||||
originatingMenuInteractionGeneration: originatingMenuInteractionGeneration)
|
||||
}
|
||||
|
||||
private func refreshMenuProviderNow(
|
||||
menuID: ObjectIdentifier,
|
||||
originatingMenuInteractionGeneration: Int)
|
||||
{
|
||||
let menu = self.openMenus[menuID] ?? self.mergedMenu.flatMap {
|
||||
ObjectIdentifier($0) == menuID ? $0 : nil
|
||||
}
|
||||
let provider = menu.flatMap { self.manualRefreshProvider(for: $0) } ?? self.menuProviders[menuID]
|
||||
self.startManualRefresh(
|
||||
for: provider,
|
||||
originatingMenuID: menuID,
|
||||
originatingMenuInteractionGeneration: originatingMenuInteractionGeneration)
|
||||
}
|
||||
|
||||
private func startManualRefresh(
|
||||
for provider: UsageProvider?,
|
||||
originatingMenuID: ObjectIdentifier?,
|
||||
originatingMenuInteractionGeneration: Int?)
|
||||
{
|
||||
let scope: ManualRefreshScope = provider.map(ManualRefreshScope.provider) ?? .global
|
||||
let scopedRefreshInFlight = provider.map { self.store.refreshingProviders.contains($0) }
|
||||
?? !self.store.refreshingProviders.isEmpty
|
||||
// Two different providers may refresh concurrently, but an all-providers (.global) refresh must
|
||||
// not overlap a per-provider one (or vice versa) — that would duplicate the shared fetch work.
|
||||
let conflictsWithOtherScope = scope == .global
|
||||
? self.manualRefreshTasks.contains { $0.key != .global }
|
||||
: self.manualRefreshTasks[.global] != nil
|
||||
guard !self.hasPreparedForAppShutdown,
|
||||
self.manualRefreshTasks[scope] == nil,
|
||||
!conflictsWithOtherScope,
|
||||
!self.store.hasForcedRefreshEnrichmentInFlight,
|
||||
!self.store.isRefreshing,
|
||||
!scopedRefreshInFlight
|
||||
else { return }
|
||||
|
||||
let frozenModels = self.frozenManualRefreshMenuCardModels()
|
||||
let viewportRestoreRequests = self.armManualRefreshViewportRestoreRequests(
|
||||
originatingMenuID: originatingMenuID,
|
||||
originatingMenuInteractionGeneration: originatingMenuInteractionGeneration)
|
||||
let task = Task { @MainActor [weak self] in
|
||||
guard let self else { return }
|
||||
var completed = false
|
||||
defer {
|
||||
self.manualRefreshTasks[scope] = nil
|
||||
self.menuCardRefreshMonitor.endManualRefresh(for: provider)
|
||||
self.updatePersistentRefreshItemsEnabled()
|
||||
if completed {
|
||||
self.scheduleCompletedManualRefreshViewportRestore(viewportRestoreRequests)
|
||||
} else {
|
||||
self.cancelManualRefreshViewportRestoreRequests(viewportRestoreRequests)
|
||||
}
|
||||
self.completeParentMenuRebuildAfterHostedSubviewCloseIfNeeded()
|
||||
self.prepareAttachedClosedMenusIfNeeded()
|
||||
}
|
||||
guard !Task.isCancelled, !self.hasPreparedForAppShutdown else { return }
|
||||
#if DEBUG
|
||||
if let operation = self._test_manualRefreshOperation {
|
||||
await operation()
|
||||
guard !Task.isCancelled, !self.hasPreparedForAppShutdown else { return }
|
||||
completed = true
|
||||
return
|
||||
}
|
||||
#endif
|
||||
if let provider {
|
||||
await self.performStoreRefresh(
|
||||
for: provider,
|
||||
refreshOpenMenusWhenComplete: true,
|
||||
interaction: .userInitiated)
|
||||
} else {
|
||||
await self.performStoreRefresh(
|
||||
enrichmentMode: .forcedBackground,
|
||||
refreshOpenMenusWhenComplete: true,
|
||||
interaction: .userInitiated)
|
||||
}
|
||||
guard !Task.isCancelled, !self.hasPreparedForAppShutdown else { return }
|
||||
completed = true
|
||||
}
|
||||
self.manualRefreshTasks[scope] = task
|
||||
self.menuCardRefreshMonitor.beginManualRefresh(frozenModels: frozenModels, provider: provider)
|
||||
self.updatePersistentRefreshItemsEnabled()
|
||||
}
|
||||
|
||||
private func manualRefreshProvider(for menu: NSMenu?) -> UsageProvider? {
|
||||
guard let menu else { return nil }
|
||||
if self.shouldMergeIcons {
|
||||
guard self.mergedMenu == nil || menu === self.mergedMenu else { return nil }
|
||||
guard !self.isMergedOverviewSelected(in: menu) else { return nil }
|
||||
return self.resolvedMenuProvider()
|
||||
}
|
||||
return self.menuProviders[ObjectIdentifier(menu)]
|
||||
}
|
||||
|
||||
private func frozenManualRefreshMenuCardModels() -> [UsageProvider: UsageMenuCardView.Model] {
|
||||
var providers = self.store.enabledProvidersForDisplay()
|
||||
if let lastMenuProvider,
|
||||
!providers.contains(lastMenuProvider)
|
||||
{
|
||||
providers.append(lastMenuProvider)
|
||||
}
|
||||
if providers.isEmpty,
|
||||
let defaultProvider = self.settings.orderedProviders().first ?? UsageProvider.allCases.first
|
||||
{
|
||||
providers.append(defaultProvider)
|
||||
}
|
||||
|
||||
var models: [UsageProvider: UsageMenuCardView.Model] = [:]
|
||||
for provider in providers {
|
||||
models[provider] = self.menuCardModel(for: provider)
|
||||
}
|
||||
return models
|
||||
}
|
||||
|
||||
func performPersistentRefreshAction(in menuID: ObjectIdentifier) {
|
||||
guard let menuInteractionGeneration = self.menuSession.menuInteractionGeneration(for: menuID) else { return }
|
||||
self.performPersistentRefreshAction(
|
||||
in: menuID,
|
||||
menuInteractionGeneration: menuInteractionGeneration)
|
||||
}
|
||||
|
||||
nonisolated func performPersistentRefreshAction(
|
||||
in menuID: ObjectIdentifier,
|
||||
menuInteractionGeneration: Int)
|
||||
{
|
||||
Task { @MainActor [weak self] in
|
||||
guard let self else { return }
|
||||
self.refreshMenuProviderNow(
|
||||
menuID: menuID,
|
||||
originatingMenuInteractionGeneration: menuInteractionGeneration)
|
||||
}
|
||||
}
|
||||
|
||||
nonisolated func performPersistentSettingsAction() {
|
||||
Task { @MainActor [weak self] in
|
||||
guard let self else { return }
|
||||
self.closeOpenMenusFromShortcutIfNeeded()
|
||||
self.showSettingsGeneral()
|
||||
}
|
||||
}
|
||||
|
||||
nonisolated func performPersistentQuitAction() {
|
||||
Task { @MainActor [weak self] in
|
||||
guard let self else { return }
|
||||
self.closeOpenMenusFromShortcutIfNeeded()
|
||||
self.quit()
|
||||
}
|
||||
}
|
||||
|
||||
nonisolated func performProviderNavigation(_ direction: StatusItemMenuProviderNavigationDirection) {
|
||||
Task { @MainActor [weak self] in
|
||||
self?.navigateProviderSwitcher(direction)
|
||||
}
|
||||
}
|
||||
|
||||
@objc func refreshAugmentSession() {
|
||||
Task {
|
||||
await self.store.forceRefreshAugmentSession()
|
||||
// Also trigger a full refresh to update the menu and clear any stale errors
|
||||
await ProviderInteractionContext.$current.withValue(.userInitiated) {
|
||||
await self.store.refresh(forceTokenUsage: false)
|
||||
}
|
||||
self.refreshOpenMenusAfterExplicitStoreAction()
|
||||
}
|
||||
}
|
||||
|
||||
@objc func installUpdate() {
|
||||
self.updater.installUpdate()
|
||||
}
|
||||
|
||||
@objc func openDashboard() {
|
||||
let preferred = self.lastMenuProvider
|
||||
?? (self.store.isEnabled(.codex) ? .codex : self.store.enabledProviders().first)
|
||||
|
||||
let provider = preferred ?? .codex
|
||||
guard let url = self.dashboardURL(for: provider) else { return }
|
||||
NSWorkspace.shared.open(url)
|
||||
}
|
||||
|
||||
func dashboardURL(
|
||||
for provider: UsageProvider,
|
||||
environment: [String: String] = ProcessInfo.processInfo.environment) -> URL?
|
||||
{
|
||||
if provider == .alibaba {
|
||||
return self.settings.alibabaCodingPlanAPIRegion.dashboardURL
|
||||
}
|
||||
if provider == .alibabatokenplan {
|
||||
return AlibabaTokenPlanUsageFetcher.dashboardURL(
|
||||
region: self.settings.alibabaTokenPlanAPIRegion,
|
||||
environment: environment)
|
||||
}
|
||||
if provider == .minimax {
|
||||
return self.settings.minimaxAPIRegion.dashboardURL
|
||||
}
|
||||
|
||||
if provider == .opencodego {
|
||||
return self.settings.opencodegoDashboardURL
|
||||
}
|
||||
|
||||
if provider == .wayfinder {
|
||||
return WayfinderProviderImplementation.dashboardURL(
|
||||
settings: self.settings,
|
||||
environment: environment)
|
||||
}
|
||||
|
||||
if provider == .zai {
|
||||
return ZaiUsageFetcher.resolveDashboardURL(
|
||||
region: self.settings.zaiAPIRegion,
|
||||
environment: environment,
|
||||
usageScope: self.settings.zaiEffectiveUsageScope())
|
||||
}
|
||||
|
||||
if provider == .qoder {
|
||||
return QoderProviderDescriptor.dashboardURL(
|
||||
settings: self.settings.qoderSettingsSnapshot(tokenOverride: nil),
|
||||
sourceLabel: self.store.sourceLabel(for: .qoder))
|
||||
}
|
||||
|
||||
let meta = self.store.metadata(for: provider)
|
||||
let urlString: String? = if provider == .claude, self.store.isClaudeSubscription() {
|
||||
meta.subscriptionDashboardURL ?? meta.dashboardURL
|
||||
} else {
|
||||
meta.dashboardURL
|
||||
}
|
||||
|
||||
guard let urlString else { return nil }
|
||||
return URL(string: urlString)
|
||||
}
|
||||
|
||||
@objc func openCreditsPurchase() {
|
||||
let preferred = self.lastMenuProvider
|
||||
?? (self.store.isEnabled(.codex) ? .codex : self.store.enabledProviders().first)
|
||||
let provider = preferred ?? .codex
|
||||
guard provider == .codex else { return }
|
||||
|
||||
let dashboardURL = self.store.metadata(for: .codex).dashboardURL
|
||||
let purchaseURL = Self.sanitizedCreditsPurchaseURL(self.store.openAIDashboard?.creditsPurchaseURL)
|
||||
let urlString = purchaseURL ?? dashboardURL
|
||||
guard let urlString,
|
||||
let url = URL(string: urlString) else { return }
|
||||
|
||||
let autoStart = true
|
||||
let accountEmail = self.store.codexAccountEmailForOpenAIDashboard()
|
||||
let cacheScope = self.store.codexCookieCacheScopeForOpenAIWeb()
|
||||
guard OpenAICreditsPurchaseWindowController.canOpenPurchaseWindow(
|
||||
accountEmail: accountEmail,
|
||||
cacheScope: cacheScope)
|
||||
else {
|
||||
self.creditsPurchaseWindow?.close()
|
||||
self.creditsPurchaseWindow = nil
|
||||
return
|
||||
}
|
||||
let controller = self.creditsPurchaseWindow ?? OpenAICreditsPurchaseWindowController()
|
||||
controller.show(
|
||||
purchaseURL: url,
|
||||
accountEmail: accountEmail,
|
||||
cacheScope: cacheScope,
|
||||
autoStartPurchase: autoStart)
|
||||
self.creditsPurchaseWindow = controller
|
||||
}
|
||||
|
||||
static func sanitizedCreditsPurchaseURL(_ raw: String?) -> String? {
|
||||
guard let raw, let url = URL(string: raw) else { return nil }
|
||||
guard Self.isAllowedChatGPTPurchaseHost(url) else { return nil }
|
||||
let pathComponents = url.pathComponents.map { $0.lowercased() }
|
||||
let allowed = ["settings", "usage", "billing", "credits"]
|
||||
guard pathComponents.contains(where: { allowed.contains($0) }) else { return nil }
|
||||
var components = URLComponents(url: url, resolvingAgainstBaseURL: false)
|
||||
components?.query = nil
|
||||
components?.fragment = nil
|
||||
return components?.url?.absoluteString ?? url.absoluteString
|
||||
}
|
||||
|
||||
private static func isAllowedChatGPTPurchaseHost(_ url: URL) -> Bool {
|
||||
guard url.scheme?.lowercased() == "https" else { return false }
|
||||
guard let host = url.host?.lowercased() else { return false }
|
||||
return host == "chatgpt.com" || host.hasSuffix(".chatgpt.com")
|
||||
}
|
||||
|
||||
@objc func openStatusPage() {
|
||||
let preferred = self.lastMenuProvider
|
||||
?? (self.store.isEnabled(.codex) ? .codex : self.store.enabledProviders().first)
|
||||
|
||||
self.openStatusPage(for: preferred ?? .codex)
|
||||
}
|
||||
|
||||
@objc func openStatusPageFromMenuItem(_ sender: NSMenuItem) {
|
||||
let provider = (sender.identifier?.rawValue).flatMap(UsageProvider.init(rawValue:))
|
||||
?? self.lastMenuProvider
|
||||
?? .codex
|
||||
self.openStatusPage(for: provider)
|
||||
}
|
||||
|
||||
private func openStatusPage(for provider: UsageProvider) {
|
||||
let meta = self.store.metadata(for: provider)
|
||||
let urlString = meta.statusPageURL ?? meta.statusLinkURL
|
||||
guard let urlString, let url = URL(string: urlString) else { return }
|
||||
NSWorkspace.shared.open(url)
|
||||
}
|
||||
|
||||
@objc func openChangelog() {
|
||||
let preferred = self.lastMenuProvider
|
||||
?? (self.store.isEnabled(.codex) ? .codex : self.store.enabledProviders().first)
|
||||
|
||||
let provider = preferred ?? .codex
|
||||
let meta = self.store.metadata(for: provider)
|
||||
guard let urlString = meta.changelogURL, let url = URL(string: urlString) else { return }
|
||||
NSWorkspace.shared.open(url)
|
||||
}
|
||||
|
||||
@objc func openTerminalCommand(_ sender: NSMenuItem) {
|
||||
let command = sender.representedObject as? String ?? "claude"
|
||||
self.openTerminal(command: command)
|
||||
}
|
||||
|
||||
@objc func openLoginToProvider(_ sender: NSMenuItem) {
|
||||
guard let urlString = sender.representedObject as? String,
|
||||
let url = URL(string: urlString) else { return }
|
||||
NSWorkspace.shared.open(url)
|
||||
}
|
||||
|
||||
@objc func addManagedCodexAccountFromMenu(_: NSMenuItem) {
|
||||
guard self.codexAccountPromotionCoordinator.isInteractionBlocked() == false else {
|
||||
self.loginLogger.info("Add Account tap ignored: Codex account change already in-flight")
|
||||
return
|
||||
}
|
||||
guard self.settings.hasUnreadableManagedCodexAccountStore == false else {
|
||||
self.presentLoginAlert(
|
||||
title: L("Managed Codex accounts unavailable"),
|
||||
message: L(
|
||||
"CodexBar could not read managed account storage. " +
|
||||
"Recover the store before adding another account."))
|
||||
return
|
||||
}
|
||||
|
||||
Task { @MainActor [weak self] in
|
||||
guard let self else { return }
|
||||
do {
|
||||
let account = try await self.managedCodexAccountCoordinator.authenticateManagedAccount()
|
||||
self.settings.selectAuthenticatedManagedCodexAccount(account)
|
||||
await ProviderInteractionContext.$current.withValue(.userInitiated) {
|
||||
await self.store.refreshCodexAccountScopedState(allowDisabled: true)
|
||||
}
|
||||
} catch {
|
||||
self.presentManagedCodexAccountError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc func requestCodexSystemPromotionFromMenu(_ sender: NSMenuItem) {
|
||||
guard let rawManagedAccountID = sender.representedObject as? String,
|
||||
let managedAccountID = UUID(uuidString: rawManagedAccountID)
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
Task { @MainActor [weak self] in
|
||||
guard let self else { return }
|
||||
let result = await self.codexAccountPromotionCoordinator.promote(managedAccountID: managedAccountID)
|
||||
if case let .failure(error) = result {
|
||||
self.presentLoginAlert(title: error.title, message: error.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc func runSwitchAccount(_ sender: NSMenuItem) {
|
||||
if self.loginTask != nil {
|
||||
self.loginLogger.info("Switch Account tap ignored: login already in-flight")
|
||||
return
|
||||
}
|
||||
|
||||
let rawProvider = sender.representedObject as? String
|
||||
let provider = rawProvider.flatMap(UsageProvider.init(rawValue:)) ?? self.lastMenuProvider ?? .codex
|
||||
self.loginLogger.info("Switch Account tapped", metadata: ["provider": provider.rawValue])
|
||||
self.startLoginFlow(provider: provider)
|
||||
}
|
||||
|
||||
func runLoginFlowFromSettings(provider: UsageProvider) async {
|
||||
guard self.loginTask == nil else {
|
||||
self.loginLogger.info(
|
||||
"Settings login tap ignored: login already in-flight",
|
||||
metadata: ["provider": provider.rawValue])
|
||||
return
|
||||
}
|
||||
self.startLoginFlow(provider: provider)
|
||||
await self.loginTask?.value
|
||||
}
|
||||
|
||||
@objc func showSettingsGeneral() {
|
||||
// Restore the last selected pane; only About navigates explicitly.
|
||||
self.openSettings(pane: nil)
|
||||
}
|
||||
|
||||
@objc func showSettingsAbout() {
|
||||
self.openSettings(pane: .about)
|
||||
}
|
||||
|
||||
func openMenuFromShortcut() {
|
||||
if self.closeOpenMenusFromShortcutIfNeeded() {
|
||||
return
|
||||
}
|
||||
|
||||
if self.shouldMergeIcons {
|
||||
self.statusItem.button?.performClick(nil)
|
||||
return
|
||||
}
|
||||
|
||||
let provider = self.resolvedShortcutProvider()
|
||||
// Use the lazy accessor to ensure the item exists
|
||||
let item = self.lazyStatusItem(for: provider)
|
||||
item.button?.performClick(nil)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func closeOpenMenusFromShortcutIfNeeded() -> Bool {
|
||||
guard !self.openMenus.isEmpty else { return false }
|
||||
|
||||
let menus = Array(self.openMenus.values)
|
||||
for menu in menus {
|
||||
menu.cancelTrackingWithoutAnimation()
|
||||
self.forgetClosedMenu(menu)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func celebrationOriginPoint(for provider: UsageProvider?) -> CGPoint? {
|
||||
let item: NSStatusItem = if self.shouldMergeIcons {
|
||||
self.statusItem
|
||||
} else if let provider, let existing = self.statusItems[provider], existing.isVisible {
|
||||
existing
|
||||
} else {
|
||||
self.lazyStatusItem(for: provider ?? .codex)
|
||||
}
|
||||
|
||||
guard let button = item.button,
|
||||
let window = button.window
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let buttonFrameInWindow = button.convert(button.bounds, to: nil)
|
||||
let screenFrame = window.convertToScreen(buttonFrameInWindow)
|
||||
return CGPoint(x: screenFrame.midX, y: screenFrame.midY)
|
||||
}
|
||||
|
||||
private func openSettings(pane: SettingsPane?) {
|
||||
DispatchQueue.main.async {
|
||||
if let pane {
|
||||
self.preferencesSelection.pane = pane
|
||||
}
|
||||
NSApp.activate(ignoringOtherApps: true)
|
||||
NotificationCenter.default.post(name: .codexbarOpenSettings, object: nil)
|
||||
}
|
||||
}
|
||||
|
||||
@objc func quit() {
|
||||
let openMenus = Array(self.openMenus.values)
|
||||
for menu in openMenus {
|
||||
menu.cancelTrackingWithoutAnimation()
|
||||
}
|
||||
|
||||
self.scheduleQuitTermination { [weak self] in
|
||||
guard let self else { return }
|
||||
self.prepareForAppShutdown()
|
||||
self.terminateApplicationForQuit()
|
||||
}
|
||||
}
|
||||
|
||||
@objc func copyError(_ sender: NSMenuItem) {
|
||||
if let err = sender.representedObject as? String {
|
||||
let pb = NSPasteboard.general
|
||||
pb.clearContents()
|
||||
pb.setString(err, forType: .string)
|
||||
}
|
||||
}
|
||||
|
||||
func openTerminal(command: String) {
|
||||
let terminal = self.settings.terminalApp
|
||||
|
||||
if terminal == .iTerm, !terminal.isInstalled {
|
||||
CodexBarLog.logger(LogCategories.terminal).warning(
|
||||
"iTerm is not installed, falling back to Terminal.app",
|
||||
metadata: ["terminal": terminal.rawValue])
|
||||
Self.openTerminalInDefaultTerminal(command: command)
|
||||
return
|
||||
}
|
||||
|
||||
if Self.executeAppleScript(terminal.appleScript(command: command)) {
|
||||
return
|
||||
}
|
||||
guard terminal != .terminal else { return }
|
||||
|
||||
CodexBarLog.logger(LogCategories.terminal).warning(
|
||||
"\(terminal.label) AppleScript failed, falling back to Terminal.app",
|
||||
metadata: ["terminal": terminal.rawValue])
|
||||
Self.openTerminalInDefaultTerminal(command: command)
|
||||
}
|
||||
|
||||
private static func openTerminalInDefaultTerminal(command: String) {
|
||||
self.executeAppleScript(TerminalApp.terminal.appleScript(command: command))
|
||||
}
|
||||
|
||||
/// Executes an AppleScript and returns `true` on success, `false` on failure.
|
||||
@discardableResult
|
||||
private static func executeAppleScript(_ source: String) -> Bool {
|
||||
if let appleScript = NSAppleScript(source: source) {
|
||||
var error: NSDictionary?
|
||||
appleScript.executeAndReturnError(&error)
|
||||
if let error {
|
||||
CodexBarLog.logger(LogCategories.terminal).error(
|
||||
"Failed to execute AppleScript",
|
||||
metadata: ["error": String(describing: error)])
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
CodexBarLog.logger(LogCategories.terminal).error("Failed to compile AppleScript")
|
||||
return false
|
||||
}
|
||||
|
||||
private func resolvedShortcutProvider() -> UsageProvider {
|
||||
if let last = self.lastMenuProvider, self.isEnabled(last) {
|
||||
return last
|
||||
}
|
||||
if let first = self.store.enabledProviders().first {
|
||||
return first
|
||||
}
|
||||
return .codex
|
||||
}
|
||||
|
||||
private func startLoginFlow(provider: UsageProvider) {
|
||||
self.loginTask = Task { @MainActor [weak self] in
|
||||
guard let self else { return }
|
||||
defer {
|
||||
self.activeLoginProvider = nil
|
||||
self.loginTask = nil
|
||||
}
|
||||
self.activeLoginProvider = provider
|
||||
self.loginPhase = .requesting
|
||||
self.loginLogger.info("Starting login task", metadata: ["provider": provider.rawValue])
|
||||
|
||||
let shouldRefresh = await self.runLoginFlow(provider: provider)
|
||||
if shouldRefresh {
|
||||
await ProviderInteractionContext.$current.withValue(.userInitiated) {
|
||||
await self.store.refresh()
|
||||
}
|
||||
self.loginLogger.info("Triggered refresh after login", metadata: ["provider": provider.rawValue])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func presentCodexLoginResult(_ result: CodexLoginRunner.Result) {
|
||||
guard let info = CodexLoginAlertPresentation.alertInfo(for: result) else { return }
|
||||
self.presentLoginAlert(title: info.title, message: info.message)
|
||||
}
|
||||
|
||||
private func presentManagedCodexAccountError(_ error: Error) {
|
||||
let info = if let error = error as? ManagedCodexAccountCoordinatorError,
|
||||
error == .authenticationInProgress
|
||||
{
|
||||
LoginAlertInfo(
|
||||
title: L("Codex account login already running"),
|
||||
message: L("Wait for the current managed Codex login to finish before adding another account."))
|
||||
} else if let error = error as? ManagedCodexAccountServiceError {
|
||||
LoginAlertInfo(title: L("Could not add Codex account"), message: error.userFacingMessage)
|
||||
} else {
|
||||
LoginAlertInfo(title: L("Could not add Codex account"), message: error.localizedDescription)
|
||||
}
|
||||
|
||||
self.presentLoginAlert(title: info.title, message: info.message)
|
||||
}
|
||||
|
||||
func presentClaudeLoginResult(_ result: ClaudeLoginRunner.Result) {
|
||||
switch result.outcome {
|
||||
case .success:
|
||||
return
|
||||
case .missingBinary:
|
||||
self.presentLoginAlert(
|
||||
title: L("Claude CLI not found"),
|
||||
message: L("Install the Claude CLI (npm i -g @anthropic-ai/claude-code) and try again."))
|
||||
case let .launchFailed(message):
|
||||
self.presentLoginAlert(title: L("Could not start Claude Code login"), message: message)
|
||||
case .timedOut:
|
||||
self.presentLoginAlert(
|
||||
title: L("Claude login timed out"),
|
||||
message: self.trimmedLoginOutput(result.output))
|
||||
case let .failed(status):
|
||||
let statusLine = String(format: L("claude auth login exited with status %d."), status)
|
||||
let message = self.trimmedLoginOutput(result.output.isEmpty ? statusLine : result.output)
|
||||
self.presentLoginAlert(title: L("Claude login failed"), message: message)
|
||||
}
|
||||
}
|
||||
|
||||
func describe(_ outcome: CodexLoginRunner.Result.Outcome) -> String {
|
||||
switch outcome {
|
||||
case .success: "success"
|
||||
case .timedOut: "timedOut"
|
||||
case let .failed(status): "failed(status: \(status))"
|
||||
case .missingBinary: "missingBinary"
|
||||
case let .launchFailed(message): "launchFailed(\(message))"
|
||||
}
|
||||
}
|
||||
|
||||
func describe(_ outcome: ClaudeLoginRunner.Result.Outcome) -> String {
|
||||
switch outcome {
|
||||
case .success: "success"
|
||||
case .timedOut: "timedOut"
|
||||
case let .failed(status): "failed(status: \(status))"
|
||||
case .missingBinary: "missingBinary"
|
||||
case let .launchFailed(message): "launchFailed(\(message))"
|
||||
}
|
||||
}
|
||||
|
||||
func describe(_ outcome: GeminiLoginRunner.Result.Outcome) -> String {
|
||||
switch outcome {
|
||||
case .success: "success"
|
||||
case .missingBinary: "missingBinary"
|
||||
case let .launchFailed(message): "launchFailed(\(message))"
|
||||
}
|
||||
}
|
||||
|
||||
func describe(_ outcome: AntigravityLoginRunner.Result.Outcome) -> String {
|
||||
switch outcome {
|
||||
case let .success(email):
|
||||
"success(email: \(email ?? "nil"))"
|
||||
case .cancelled:
|
||||
"cancelled"
|
||||
case .timedOut:
|
||||
"timedOut"
|
||||
case let .launchFailed(message):
|
||||
"launchFailed(\(message))"
|
||||
case let .failed(message):
|
||||
"failed(\(message))"
|
||||
}
|
||||
}
|
||||
|
||||
func presentGeminiLoginResult(_ result: GeminiLoginRunner.Result) {
|
||||
guard let info = Self.geminiLoginAlertInfo(for: result) else { return }
|
||||
self.presentLoginAlert(title: info.title, message: info.message)
|
||||
}
|
||||
|
||||
func presentAntigravityLoginResult(_ result: AntigravityLoginRunner.Result) {
|
||||
guard let info = Self.antigravityLoginAlertInfo(for: result) else { return }
|
||||
self.presentLoginAlert(title: info.title, message: info.message)
|
||||
}
|
||||
|
||||
struct LoginAlertInfo: Equatable {
|
||||
let title: String
|
||||
let message: String
|
||||
}
|
||||
|
||||
nonisolated static func geminiLoginAlertInfo(for result: GeminiLoginRunner.Result) -> LoginAlertInfo? {
|
||||
switch result.outcome {
|
||||
case .success:
|
||||
nil
|
||||
case .missingBinary:
|
||||
LoginAlertInfo(
|
||||
title: L("Gemini CLI not found"),
|
||||
message: L("Install the Gemini CLI (npm i -g @google/gemini-cli) and try again."))
|
||||
case let .launchFailed(message):
|
||||
LoginAlertInfo(title: L("Could not open Terminal for Gemini"), message: message)
|
||||
}
|
||||
}
|
||||
|
||||
nonisolated static func antigravityLoginAlertInfo(for result: AntigravityLoginRunner.Result) -> LoginAlertInfo? {
|
||||
switch result.outcome {
|
||||
case .success, .cancelled:
|
||||
nil
|
||||
case .timedOut:
|
||||
LoginAlertInfo(
|
||||
title: L("Antigravity login timed out"),
|
||||
message: L("The browser login did not complete in time. Try Antigravity login again."))
|
||||
case let .launchFailed(message):
|
||||
LoginAlertInfo(
|
||||
title: L("Could not open browser for Antigravity"),
|
||||
message: String(format: L("Open this URL manually to continue login:\n\n%@"), message))
|
||||
case let .failed(message):
|
||||
LoginAlertInfo(title: L("Antigravity login failed"), message: message)
|
||||
}
|
||||
}
|
||||
|
||||
func presentLoginAlert(title: String, message: String) {
|
||||
let alert = NSAlert()
|
||||
alert.messageText = L(title)
|
||||
alert.informativeText = L(message)
|
||||
alert.alertStyle = .warning
|
||||
alert.runModal()
|
||||
}
|
||||
|
||||
private func trimmedLoginOutput(_ text: String) -> String {
|
||||
let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let limit = 600
|
||||
if trimmed.isEmpty {
|
||||
return L("No output captured.")
|
||||
}
|
||||
if trimmed.count <= limit {
|
||||
return trimmed
|
||||
}
|
||||
let idx = trimmed.index(trimmed.startIndex, offsetBy: limit)
|
||||
return "\(trimmed[..<idx])…"
|
||||
}
|
||||
|
||||
func postLoginNotification(for provider: UsageProvider) {
|
||||
let name = ProviderDescriptorRegistry.descriptor(for: provider).metadata.displayName
|
||||
let (title, body) = LoginNotificationLogic.notificationCopy(providerName: name)
|
||||
AppNotifications.shared.post(idPrefix: "login-\(provider.rawValue)", title: title, body: body)
|
||||
}
|
||||
|
||||
func presentCursorLoginResult(_ result: CursorLoginRunner.Result) {
|
||||
switch result.outcome {
|
||||
case .success:
|
||||
return
|
||||
case .cancelled:
|
||||
// User closed the window; no alert needed
|
||||
return
|
||||
case let .failed(message):
|
||||
self.presentLoginAlert(title: L("Cursor login failed"), message: message)
|
||||
}
|
||||
}
|
||||
|
||||
func describe(_ outcome: CursorLoginRunner.Result.Outcome) -> String {
|
||||
switch outcome {
|
||||
case .success: "success"
|
||||
case .cancelled: "cancelled"
|
||||
case let .failed(message): "failed(\(message))"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user