Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Sources/CodexBar/CodexbarApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -333,4 +333,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
self.updaterController,
PreferencesSelection())
}

func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
return false
}
}
2 changes: 1 addition & 1 deletion Sources/CodexBar/HiddenWindowView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 1 addition & 2 deletions Sources/CodexBar/StatusItemController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
40 changes: 28 additions & 12 deletions Sources/CodexBarCore/UsageFetcher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Comment on lines +638 to +641

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Fall back to API key when JWT parse fails

If auth.json contains both tokens.idToken and OPENAI_API_KEY (e.g., a user switches to API key auth but an old/empty/invalid idToken remains), the current logic returns early on parseJWT failure and never reaches the API key fallback. That means API key auth still fails in this mixed state even though the key is valid. Consider falling back to the API key path when parseJWT returns nil instead of returning immediately.

Useful? React with 👍 / 👎.


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
Expand Down Expand Up @@ -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
}
}