Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
- Settings: keep the sidebar fixed and visible while resizing, prevent collapse or over-expansion, and cap detail content width for readability. Thanks @Zihao-Qi!

### Fixed
- Providers: detect Claude Desktop on fresh installs and ignore Gemini CLI installations without usable OAuth credentials.
- Claude: give multiple claude-swap accounts precedence over token-account cards and segmented switching so adapter rows remain visible. Thanks @optimiz-r!
- Menus: scope manual refresh state to the provider being refreshed, allowing independent provider refreshes without greying unrelated rows. Thanks @hhh2210!
- Claude history: quarantine same-directory account-switch samples until credential ownership is stable, preventing plan-utilization history from crossing accounts. Thanks @ss251!
Expand Down
73 changes: 52 additions & 21 deletions Sources/CodexBar/SettingsStore+ProviderDetection.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,30 @@
import AppKit
import CodexBarCore
import Foundation

enum ProviderDetectionPolicy {
struct Signals {
let codexCLIInstalled: Bool
let claudeCLIInstalled: Bool
let claudeDesktopInstalled: Bool
let geminiCLIInstalled: Bool
let geminiConfigured: Bool
let antigravityAvailable: Bool
}

static func enabledProviders(signals: Signals) -> Set<UsageProvider> {
var enabled: Set<UsageProvider> = []
if signals.codexCLIInstalled { enabled.insert(.codex) }
if signals.claudeCLIInstalled || signals.claudeDesktopInstalled { enabled.insert(.claude) }
if signals.geminiCLIInstalled, signals.geminiConfigured { enabled.insert(.gemini) }
if signals.antigravityAvailable { enabled.insert(.antigravity) }

// Keep the historical Codex default when no usable provider source is found.
if enabled.isEmpty { enabled.insert(.codex) }
return enabled
}
}

extension SettingsStore {
func runInitialProviderDetectionIfNeeded(force: Bool = false) {
guard force || !self.providerDetectionCompleted else { return }
Expand All @@ -13,51 +37,58 @@ extension SettingsStore {

func applyProviderDetection() async {
guard !self.providerDetectionCompleted else { return }
let codexInstalled = BinaryLocator.resolveCodexBinary() != nil
let claudeInstalled = BinaryLocator.resolveClaudeBinary() != nil
let geminiInstalled = BinaryLocator.resolveGeminiBinary() != nil
let codexCLIInstalled = BinaryLocator.resolveCodexBinary() != nil
let claudeCLIInstalled = BinaryLocator.resolveClaudeBinary() != nil
let claudeDesktopInstalled = NSWorkspace.shared.urlForApplication(
withBundleIdentifier: "com.anthropic.claudefordesktop") != nil
Comment on lines +42 to +43

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Do not enable Claude for desktop-only installs

When Claude Desktop is installed but the standalone claude binary is not on PATH/well-known paths and there is no browser Claude session, this signal makes Claude the only enabled provider and skips the Codex fallback. The Claude fetch planner still gets CLI availability from ClaudeCLIResolver.isAvailable/BinaryLocator.claudeWellKnownPaths, which has no Desktop app path, and the web fallback only probes browser cookies, so a desktop-only fresh install can start with an enabled Claude card that has no usable source. Please either resolve Desktop credentials/CLI before counting it as available or keep the fallback in this case.

Useful? React with 👍 / 👎.

let geminiCLIInstalled = BinaryLocator.resolveGeminiBinary() != nil
let geminiConfigured = FileManager.default.fileExists(
atPath: FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent(".gemini/oauth_creds.json").path)
let antigravityRunning = await AntigravityStatusProbe.isRunning()
let antigravityLoggedIn = FileManager.default.fileExists(
atPath: AntigravityOAuthCredentialsStore().fileURL.path)
let logger = CodexBarLog.logger(LogCategories.providerDetection)

// If none installed, keep Codex enabled to match previous behavior.
let noneInstalled = !codexInstalled && !claudeInstalled && !geminiInstalled && !antigravityRunning &&
!antigravityLoggedIn
let enableCodex = codexInstalled || noneInstalled
let enableClaude = claudeInstalled
let enableGemini = geminiInstalled
let enableAntigravity = antigravityRunning || antigravityLoggedIn
let enabledProviders = ProviderDetectionPolicy.enabledProviders(signals: .init(
codexCLIInstalled: codexCLIInstalled,
claudeCLIInstalled: claudeCLIInstalled,
claudeDesktopInstalled: claudeDesktopInstalled,
geminiCLIInstalled: geminiCLIInstalled,
geminiConfigured: geminiConfigured,
antigravityAvailable: antigravityRunning || antigravityLoggedIn))

logger.info(
"Provider detection results",
metadata: [
"codexInstalled": codexInstalled ? "1" : "0",
"claudeInstalled": claudeInstalled ? "1" : "0",
"geminiInstalled": geminiInstalled ? "1" : "0",
"codexCLIInstalled": codexCLIInstalled ? "1" : "0",
"claudeCLIInstalled": claudeCLIInstalled ? "1" : "0",
"claudeDesktopInstalled": claudeDesktopInstalled ? "1" : "0",
"geminiCLIInstalled": geminiCLIInstalled ? "1" : "0",
"geminiConfigured": geminiConfigured ? "1" : "0",
"antigravityRunning": antigravityRunning ? "1" : "0",
"antigravityLoggedIn": antigravityLoggedIn ? "1" : "0",
])
logger.info(
"Provider detection enablement",
metadata: [
"codex": enableCodex ? "1" : "0",
"claude": enableClaude ? "1" : "0",
"gemini": enableGemini ? "1" : "0",
"antigravity": enableAntigravity ? "1" : "0",
"codex": enabledProviders.contains(.codex) ? "1" : "0",
"claude": enabledProviders.contains(.claude) ? "1" : "0",
"gemini": enabledProviders.contains(.gemini) ? "1" : "0",
"antigravity": enabledProviders.contains(.antigravity) ? "1" : "0",
])

self.updateProviderConfig(provider: .codex) { entry in
entry.enabled = enableCodex
entry.enabled = enabledProviders.contains(.codex)
}
self.updateProviderConfig(provider: .claude) { entry in
entry.enabled = enableClaude
entry.enabled = enabledProviders.contains(.claude)
}
self.updateProviderConfig(provider: .gemini) { entry in
entry.enabled = enableGemini
entry.enabled = enabledProviders.contains(.gemini)
}
self.updateProviderConfig(provider: .antigravity) { entry in
entry.enabled = enableAntigravity
entry.enabled = enabledProviders.contains(.antigravity)
}
self.providerDetectionCompleted = true
logger.info("Provider detection completed")
Expand Down
44 changes: 44 additions & 0 deletions Tests/CodexBarTests/ProviderDetectionPolicyTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import Testing
@testable import CodexBar
@testable import CodexBarCore

struct ProviderDetectionPolicyTests {
@Test
func `fresh install detects Codex and Claude Desktop without unconfigured Gemini`() {
let enabled = ProviderDetectionPolicy.enabledProviders(signals: .init(
codexCLIInstalled: true,
claudeCLIInstalled: false,
claudeDesktopInstalled: true,
geminiCLIInstalled: true,
geminiConfigured: false,
antigravityAvailable: false))

#expect(enabled == [.codex, .claude])
}

@Test
func `configured Gemini CLI is detected`() {
let enabled = ProviderDetectionPolicy.enabledProviders(signals: .init(
codexCLIInstalled: false,
claudeCLIInstalled: false,
claudeDesktopInstalled: false,
geminiCLIInstalled: true,
geminiConfigured: true,
antigravityAvailable: false))

#expect(enabled == [.gemini])
}

@Test
func `Codex remains the fallback when no provider source is available`() {
let enabled = ProviderDetectionPolicy.enabledProviders(signals: .init(
codexCLIInstalled: false,
claudeCLIInstalled: false,
claudeDesktopInstalled: false,
geminiCLIInstalled: false,
geminiConfigured: false,
antigravityAvailable: false))

#expect(enabled == [.codex])
}
}