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 @@ -6,6 +6,7 @@
- Settings: split provider pane "Settings" sections into "Menu bar" and "Connection" so metric pickers and auth/cookie/source controls are grouped by topic.

### Fixed
- Claude login: preserve the selected usage source after OAuth sign-in so Auto mode can still fall back to CLI or web data when OAuth is unavailable. Thanks @Chipagosfinest!
- Claude quotas: ignore synthetic no-session placeholders when tracking notifications, history, and reset events, preventing false restores and duplicate threshold or pace warnings while weekly usage continues updating. Thanks @vincent-peng!
- German localization: label manual cookie-source and refresh options as “Manuell” instead of the handbook noun “Handbuch.” Thanks @fbrettnich!
- Amp: parse the current percentage-based daily Amp Free usage output while preserving individual and workspace balances. Thanks @3kh0!
Expand Down
15 changes: 13 additions & 2 deletions Sources/CodexBar/Providers/Claude/ClaudeLoginFlow.swift
Original file line number Diff line number Diff line change
@@ -1,8 +1,20 @@
import CodexBarCore
import Foundation

typealias ClaudeLoginFlowRunner = (
_ timeout: TimeInterval,
_ onPhaseChange: @escaping @Sendable (ClaudeLoginRunner.Phase) -> Void) async -> ClaudeLoginRunner.Result

@MainActor
extension StatusItemController {
func runClaudeLoginFlow() async -> Bool {
await self.runClaudeLoginFlow(
loginRunner: { timeout, onPhaseChange in
await ClaudeLoginRunner.run(timeout: timeout, onPhaseChange: onPhaseChange)
})
}

func runClaudeLoginFlow(loginRunner: ClaudeLoginFlowRunner) async -> Bool {
let phaseHandler: @Sendable (ClaudeLoginRunner.Phase) -> Void = { [weak self] phase in
Task { @MainActor in
switch phase {
Expand All @@ -11,7 +23,7 @@ extension StatusItemController {
}
}
}
let result = await ClaudeLoginRunner.run(timeout: 120, onPhaseChange: phaseHandler)
let result = await loginRunner(120, phaseHandler)
guard !Task.isCancelled else { return false }
self.loginPhase = .idle
self.presentClaudeLoginResult(result)
Expand All @@ -21,7 +33,6 @@ extension StatusItemController {
if case .success = result.outcome {
let metadata = self.store.metadata(for: .claude)
self.settings.setProviderEnabled(provider: .claude, metadata: metadata, enabled: true)
self.settings.claudeUsageDataSource = .oauth
self.postLoginNotification(for: .claude)
return true
}
Expand Down
48 changes: 48 additions & 0 deletions Tests/CodexBarTests/ClaudeLoginFlowPolicyTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import CodexBarCore
import Testing
@testable import CodexBar

@MainActor
@Suite(.serialized)
struct ClaudeLoginFlowTests {
@Test
func `successful Claude login controller flow preserves selected source and enables provider`() async throws {
let registry = ProviderRegistry.shared
let claudeMetadata = try #require(registry.metadata[.claude])

for source in ClaudeUsageDataSource.allCases {
let settings = testSettingsStore(
suiteName: "ClaudeLoginFlowTests-controller-\(source.rawValue)")
settings.statusChecksEnabled = false
settings.refreshFrequency = .manual
settings.providerDetectionCompleted = true
settings.claudeUsageDataSource = source
settings.setProviderEnabled(provider: .claude, metadata: claudeMetadata, enabled: false)

let fetcher = UsageFetcher()
let store = UsageStore(
fetcher: fetcher,
browserDetection: BrowserDetection(cacheTTL: 0),
settings: settings)

await withStatusItemControllerForTesting(store: store, settings: settings, fetcher: fetcher) { controller in
let didLogin = await controller.runClaudeLoginFlow { _, onPhaseChange in
onPhaseChange(.requesting)
await Task.yield()
onPhaseChange(.waitingBrowser)
await Task.yield()
return ClaudeLoginRunner.Result(
outcome: .success,
output: "Successfully logged in",
authLink: nil)
}

#expect(didLogin)
#expect(controller.loginPhase == .idle)
}

#expect(settings.claudeUsageDataSource == source)
#expect(settings.isProviderEnabledCached(provider: .claude, metadataByProvider: registry.metadata))
}
}
}
3 changes: 2 additions & 1 deletion docs/claude.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@ Admin API key setup:
- `seven_day_routines` / `seven_day_cowork` → Daily Routines extra window.
- Claude Design/Omelette keys are ignored because Claude Design shares the main Claude usage limit.
- `extra_usage` → Extra usage cost (monthly spend/limit).
- Successful OAuth login enables Claude and selects OAuth as the usage source.
- Successful OAuth login enables Claude and preserves the selected usage source. With the default Auto source, OAuth
remains preferred when readable, while CLI/Web fallback stays available when OAuth credentials are not usable.
- Plan inference: `subscriptionType` is preferred when present; `rate_limit_tier` falls back to
Max/Pro/Team/Enterprise. When a Max `rate_limit_tier` carries a usage multiplier
(`default_claude_max_5x` / `default_claude_max_20x`), it is surfaced in the label as "Max 5x" / "Max 20x".
Expand Down