From 19d68c92cb39215417510e70ea0c5e27ccfc423f Mon Sep 17 00:00:00 2001 From: Anudeep Adiraju <63069338+anudeepadi@users.noreply.github.com> Date: Mon, 9 Feb 2026 15:54:25 -0600 Subject: [PATCH] Fix menu bar persistence and Codex API key authentication ## Menu Bar Persistence Fixes - Add applicationShouldTerminateAfterLastWindowClosed to prevent app termination when all windows close - Replace .transient with .stationary for hidden window to survive space transitions - Set merged icon to always be visible regardless of provider state - Remove unused anyEnabled variable These changes ensure the menu bar icon persists through space switches, window closures, and when no providers are enabled. ## Codex API Key Authentication Fix - Update AuthFile struct to include OPENAI_API_KEY field for API key support - Add fallback logic in loadAccountInfo() to handle API key authentication format - Return "API Key User" when authenticated via API key (JWT claims unavailable) Previously, loadAccountInfo() only supported OAuth tokens with JWT idToken, causing API key authentication to fail silently. This fix ensures CodexBar recognizes both OAuth and API key authentication methods from ~/.codex/auth.json. --- Sources/CodexBar/CodexbarApp.swift | 4 +++ Sources/CodexBar/HiddenWindowView.swift | 2 +- Sources/CodexBar/StatusItemController.swift | 3 +- Sources/CodexBarCore/UsageFetcher.swift | 40 ++++++++++++++------- 4 files changed, 34 insertions(+), 15 deletions(-) diff --git a/Sources/CodexBar/CodexbarApp.swift b/Sources/CodexBar/CodexbarApp.swift index f870a2405..d8d391b38 100644 --- a/Sources/CodexBar/CodexbarApp.swift +++ b/Sources/CodexBar/CodexbarApp.swift @@ -333,4 +333,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate { self.updaterController, PreferencesSelection()) } + + func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return false + } } diff --git a/Sources/CodexBar/HiddenWindowView.swift b/Sources/CodexBar/HiddenWindowView.swift index 689a2f144..fb6662301 100644 --- a/Sources/CodexBar/HiddenWindowView.swift +++ b/Sources/CodexBar/HiddenWindowView.swift @@ -21,7 +21,7 @@ struct HiddenWindowView: View { if let window = NSApp.windows.first(where: { $0.title == "CodexBarLifecycleKeepalive" }) { // Make the keepalive window truly invisible and non-interactive. window.styleMask = [.borderless] - window.collectionBehavior = [.auxiliary, .ignoresCycle, .transient, .canJoinAllSpaces] + window.collectionBehavior = [.auxiliary, .ignoresCycle, .stationary, .canJoinAllSpaces] window.isExcludedFromWindowsMenu = true window.level = .floating window.isOpaque = false diff --git a/Sources/CodexBar/StatusItemController.swift b/Sources/CodexBar/StatusItemController.swift index dce63bf76..a0287831d 100644 --- a/Sources/CodexBar/StatusItemController.swift +++ b/Sources/CodexBar/StatusItemController.swift @@ -336,11 +336,10 @@ final class StatusItemController: NSObject, NSMenuDelegate, StatusItemControllin } private func updateVisibility() { - let anyEnabled = !self.store.enabledProviders().isEmpty let force = self.store.debugForceAnimation let mergeIcons = self.shouldMergeIcons if mergeIcons { - self.statusItem.isVisible = anyEnabled || force + self.statusItem.isVisible = true // Merged icon always visible; fallback menu handles empty state for item in self.statusItems.values { item.isVisible = false } diff --git a/Sources/CodexBarCore/UsageFetcher.swift b/Sources/CodexBarCore/UsageFetcher.swift index ca300ea9f..0d5e18917 100644 --- a/Sources/CodexBarCore/UsageFetcher.swift +++ b/Sources/CodexBarCore/UsageFetcher.swift @@ -629,26 +629,36 @@ public struct UsageFetcher: Sendable { let authURL = URL(fileURLWithPath: self.environment["CODEX_HOME"] ?? "\(NSHomeDirectory())/.codex") .appendingPathComponent("auth.json") guard let data = try? Data(contentsOf: authURL), - let auth = try? JSONDecoder().decode(AuthFile.self, from: data), - let idToken = auth.tokens?.idToken + let auth = try? JSONDecoder().decode(AuthFile.self, from: data) else { return AccountInfo(email: nil, plan: nil) } - guard let payload = UsageFetcher.parseJWT(idToken) else { - return AccountInfo(email: nil, plan: nil) - } + // Try OAuth token path first (has email/plan info in JWT) + if let idToken = auth.tokens?.idToken { + guard let payload = UsageFetcher.parseJWT(idToken) else { + return AccountInfo(email: nil, plan: nil) + } + + let authDict = payload["https://api.openai.com/auth"] as? [String: Any] + let profileDict = payload["https://api.openai.com/profile"] as? [String: Any] + + let plan = (authDict?["chatgpt_plan_type"] as? String) + ?? (payload["chatgpt_plan_type"] as? String) - let authDict = payload["https://api.openai.com/auth"] as? [String: Any] - let profileDict = payload["https://api.openai.com/profile"] as? [String: Any] + let email = (payload["email"] as? String) + ?? (profileDict?["email"] as? String) - let plan = (authDict?["chatgpt_plan_type"] as? String) - ?? (payload["chatgpt_plan_type"] as? String) + return AccountInfo(email: email, plan: plan) + } - let email = (payload["email"] as? String) - ?? (profileDict?["email"] as? String) + // Fall back to API key path (no email/plan info available) + if let apiKey = auth.OPENAI_API_KEY, !apiKey.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + // API key authentication is valid, but doesn't provide email/plan + return AccountInfo(email: "API Key User", plan: nil) + } - return AccountInfo(email: email, plan: plan) + return AccountInfo(email: nil, plan: nil) } // MARK: - Helpers @@ -690,4 +700,10 @@ public struct UsageFetcher: Sendable { private struct AuthFile: Decodable { struct Tokens: Decodable { let idToken: String? } let tokens: Tokens? + let OPENAI_API_KEY: String? + + enum CodingKeys: String, CodingKey { + case tokens + case OPENAI_API_KEY + } }