feat: opt-in Claude Connect + stable dev signing#15
Conversation
…ev signing The app no longer reads the Claude Code token from the Keychain at launch, so nothing prompts for credentials on startup. A persisted `claudeConnected` flag (default off) gates `allowKeychain`; the Claude tab shows a Connect banner and Settings → Claude has a Connect/Disconnect control. Clicking Connect reads the token in a user-initiated context (macOS asks once, then remembers). Account + cost data still load without connecting. Also: Scripts/build-app.sh auto-signs local builds with an installed Developer ID (stable signature → no per-rebuild Keychain re-prompt), falling back to ad-hoc via awk (exits 0 on no match, so `set -euo pipefail` doesn't abort for cert-less contributors). Explicit SIGN_IDENTITY still overrides; CI unaffected.
📝 WalkthroughWalkthroughChangesClaude opt-in release
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant User
participant ClaudeConnectBanner
participant AppModel
participant UsageConfig
participant Keychain
User->>ClaudeConnectBanner: Click Connect
ClaudeConnectBanner->>AppModel: connectClaude()
AppModel->>UsageConfig: Set allowKeychain to true
AppModel->>Keychain: Read Claude token during refresh
Keychain-->>AppModel: Return token
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@Sources/AIUsageBarUI/AppModel.swift`:
- Around line 396-415: Update connectClaude() and disconnectClaude() to use the
existing isApplying mechanism when changing claudeConnected, bypassing its
didSet-triggered update task. Within one Task, apply the configuration change
through the service update first, then await refresh(), ensuring both operations
are sequenced; preserve the existing guards and connection-state behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: a6a3581e-65fc-4ccd-8fba-329fa64995f1
📒 Files selected for processing (7)
CHANGELOG.mdREADME.mdScripts/build-app.shSources/AIUsageBarUI/AppModel.swiftSources/AIUsageBarUI/MenuContentView.swiftSources/AIUsageBarUI/SettingsView.swiftTests/AIUsageBarUITests/ProviderSettingsTests.swift
| defaults.set(claudeConnected, forKey: "claudeConnected") | ||
| providerSettings.save(to: defaults) | ||
| } | ||
|
|
||
| /// Opt in to live Claude limits. Reads the Claude Code token from the Keychain | ||
| /// (macOS asks once, then remembers) and refreshes immediately so the prompt | ||
| /// appears in response to the user's click, not silently at launch. | ||
| public func connectClaude() { | ||
| guard !claudeConnected else { return } | ||
| claudeConnected = true // didSet → reconfigure with allowKeychain = true | ||
| Task { await refresh() } | ||
| } | ||
|
|
||
| /// Stop reading the Keychain / live endpoint; keep local account + cost data. | ||
| public func disconnectClaude() { | ||
| guard claudeConnected else { return } | ||
| claudeConnected = false | ||
| Task { await refresh() } | ||
| } | ||
|
|
There was a problem hiding this comment.
🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win
Race condition between configuration update and refresh.
Mutating claudeConnected synchronously triggers its didSet handler, which eventually spawns an unstructured Task { await service.update(config:) }. Immediately after, connectClaude()/disconnectClaude() spawn another Task { await refresh() }.
Because Swift Concurrency does not guarantee the execution order of these tasks, refresh() may execute before service.update() completes. If this happens, the refresh will use the stale configuration (e.g. failing to read the Keychain right after connecting), missing the immediate UI update until the next polling cycle.
Bypass the didSet handler using isApplying to properly sequence the service update and refresh within a single Task.
🔒️ Proposed fix to sequence the tasks safely
/// Opt in to live Claude limits. Reads the Claude Code token from the Keychain
/// (macOS asks once, then remembers) and refreshes immediately so the prompt
/// appears in response to the user's click, not silently at launch.
public func connectClaude() {
guard !claudeConnected else { return }
- claudeConnected = true // didSet → reconfigure with allowKeychain = true
- Task { await refresh() }
+ updateConnectionState(true)
}
/// Stop reading the Keychain / live endpoint; keep local account + cost data.
public func disconnectClaude() {
guard claudeConnected else { return }
- claudeConnected = false
- Task { await refresh() }
+ updateConnectionState(false)
+ }
+
+ private func updateConnectionState(_ connected: Bool) {
+ isApplying = true
+ claudeConnected = connected
+ isApplying = false
+ persist()
+
+ let config = Self.usageConfig(providerSettings: providerSettings,
+ codexEnabled: codexEnabled,
+ claudeEnabled: claudeEnabled,
+ geminiEnabled: geminiEnabled,
+ claudeConnected: connected,
+ discovered: discoverClaudeProfiles())
+ appliedClaudeProfiles = Self.effectiveClaudeProfiles(for: config)
+ if !claudeEnabled { lastClaude = [] }
+
+ Task {
+ await service.update(config: config)
+ await refresh()
+ }
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| defaults.set(claudeConnected, forKey: "claudeConnected") | |
| providerSettings.save(to: defaults) | |
| } | |
| /// Opt in to live Claude limits. Reads the Claude Code token from the Keychain | |
| /// (macOS asks once, then remembers) and refreshes immediately so the prompt | |
| /// appears in response to the user's click, not silently at launch. | |
| public func connectClaude() { | |
| guard !claudeConnected else { return } | |
| claudeConnected = true // didSet → reconfigure with allowKeychain = true | |
| Task { await refresh() } | |
| } | |
| /// Stop reading the Keychain / live endpoint; keep local account + cost data. | |
| public func disconnectClaude() { | |
| guard claudeConnected else { return } | |
| claudeConnected = false | |
| Task { await refresh() } | |
| } | |
| defaults.set(claudeConnected, forKey: "claudeConnected") | |
| providerSettings.save(to: defaults) | |
| } | |
| /// Opt in to live Claude limits. Reads the Claude Code token from the Keychain | |
| /// (macOS asks once, then remembers) and refreshes immediately so the prompt | |
| /// appears in response to the user's click, not silently at launch. | |
| public func connectClaude() { | |
| guard !claudeConnected else { return } | |
| updateConnectionState(true) | |
| } | |
| /// Stop reading the Keychain / live endpoint; keep local account + cost data. | |
| public func disconnectClaude() { | |
| guard claudeConnected else { return } | |
| updateConnectionState(false) | |
| } | |
| private func updateConnectionState(_ connected: Bool) { | |
| isApplying = true | |
| claudeConnected = connected | |
| isApplying = false | |
| persist() | |
| let config = Self.usageConfig(providerSettings: providerSettings, | |
| codexEnabled: codexEnabled, | |
| claudeEnabled: claudeEnabled, | |
| geminiEnabled: geminiEnabled, | |
| claudeConnected: connected, | |
| discovered: discoverClaudeProfiles()) | |
| appliedClaudeProfiles = Self.effectiveClaudeProfiles(for: config) | |
| if !claudeEnabled { lastClaude = [] } | |
| Task { | |
| await service.update(config: config) | |
| await refresh() | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@Sources/AIUsageBarUI/AppModel.swift` around lines 396 - 415, Update
connectClaude() and disconnectClaude() to use the existing isApplying mechanism
when changing claudeConnected, bypassing its didSet-triggered update task.
Within one Task, apply the configuration change through the service update
first, then await refresh(), ensuring both operations are sequenced; preserve
the existing guards and connection-state behavior.
Makes reading the Claude token from the Keychain opt-in, so nothing prompts for credentials at launch.
claudeConnected(default off) gatesallowKeychain; Connect banner in the Claude tab + a Connect/Disconnect control in Settings → Claude. Account + cost still load without connecting.build-app.shauto-signs local builds with an installed Developer ID (stable → no per-rebuild re-prompt); ad-hoc fallback viaawkso cert-less contributors aren't aborted byset -euo pipefail.Adversarially reviewed (4 dimensions). Fixed the confirmed build-script abort bug; documented the intentional opt-in migration (upgraders click Connect once). Tests: 11 UI + 15 Core green, incl. a new connect-gate test.
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes
Documentation