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 + } }