From 6b1d76d7dfc686603a9eb112df968637e485e716 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 5 Jul 2026 11:55:04 -0700 Subject: [PATCH] fix: detect usable providers on first launch --- CHANGELOG.md | 1 + .../SettingsStore+ProviderDetection.swift | 73 +++++++++++++------ .../ProviderDetectionPolicyTests.swift | 44 +++++++++++ 3 files changed, 97 insertions(+), 21 deletions(-) create mode 100644 Tests/CodexBarTests/ProviderDetectionPolicyTests.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index a97238047e..92259bc30a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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! diff --git a/Sources/CodexBar/SettingsStore+ProviderDetection.swift b/Sources/CodexBar/SettingsStore+ProviderDetection.swift index d4efded24a..e2f35b3491 100644 --- a/Sources/CodexBar/SettingsStore+ProviderDetection.swift +++ b/Sources/CodexBar/SettingsStore+ProviderDetection.swift @@ -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 { + var enabled: Set = [] + 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 } @@ -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 + 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") diff --git a/Tests/CodexBarTests/ProviderDetectionPolicyTests.swift b/Tests/CodexBarTests/ProviderDetectionPolicyTests.swift new file mode 100644 index 0000000000..a3ad4216ac --- /dev/null +++ b/Tests/CodexBarTests/ProviderDetectionPolicyTests.swift @@ -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]) + } +}