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 @@ -5,6 +5,7 @@
### Added
- Usage & Spend: add an All time range alongside 7d/30d, backed by 365 days of local history with dedicated Claude and Cursor spend snapshot slots (#3009). Thanks @Yuxin-Qiao!
- General settings: turn Low Power Mode into an Off/On/Automatic preference, with Automatic following the system Low Power Mode state (#2995). Thanks @elijahfriedman!
- Grok: add an Auto / Grok CLI / SuperGrok OAuth / Browser cookies source picker, support pasted SuperGrok bearers and grok.com cookies in token accounts, and open `~/.grok/auth.json` from Open token file (#3010). Thanks @oakimov!

### Fixed
- OpenCode Go: surface expired selected session tokens instead of silently replacing failed server usage with local quota estimates (#2993). Thanks @Niclassslua!
Expand Down
3 changes: 3 additions & 0 deletions Sources/CodexBar/PreferencesProvidersPane.swift
Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,9 @@ struct ProvidersPane: View {
}
} },
openConfigFile: {
if implementation?.openTokenFile(context: context) == true {
return
}
self.settings.openTokenAccountsFile()
},
reloadFromDisk: {
Expand Down
57 changes: 44 additions & 13 deletions Sources/CodexBar/ProviderRegistry.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,11 @@ struct ProviderRegistry {
provider: provider,
settings: settings,
override: nil)
let sourceMode = ProviderCatalog.implementation(for: provider)?
.sourceMode(context: ProviderSourceModeContext(provider: provider, settings: settings))
?? .auto
let sourceMode = Self.resolvedSourceMode(
provider: provider,
settings: settings,
account: account)

let snapshot = Self.makeSettingsSnapshot(settings: settings, tokenOverride: nil)
let env = Self.makeEnvironment(
base: environmentBase,
Expand All @@ -56,7 +58,8 @@ struct ProviderRegistry {
runtime: .app,
sourceMode: sourceMode,
includeCredits: false,
includeOptionalUsage: ProviderTokenAccountSelection.shouldIncludeOptionalUsage(
includeOptionalUsage:
ProviderTokenAccountSelection.shouldIncludeOptionalUsage(
provider: provider,
settings: settings,
override: nil),
Expand Down Expand Up @@ -88,7 +91,8 @@ struct ProviderRegistry {
costUsageHistoryDays: settings.costUsageHistoryDays,
persistsCLISessions: true,
persistentCLISessionIdleWindow: Self.persistentCLISessionIdleWindow(
refreshInterval: Self.nominalRefreshInterval(for: settings.refreshFrequency)))
refreshInterval: Self.nominalRefreshInterval(
for: settings.refreshFrequency)))
})
specs[provider] = spec
}
Expand All @@ -105,7 +109,23 @@ struct ProviderRegistry {
/// when specs are built, so `.adaptive` maps to the policy's nominal interval instead of a
/// live decision; `.manual` stays nil.
static func nominalRefreshInterval(for frequency: RefreshFrequency) -> TimeInterval? {
frequency.usesAdaptivePolicy ? AdaptiveRefreshPolicy.nominalIntervalForHeuristics : frequency.seconds
frequency.usesAdaptivePolicy
? AdaptiveRefreshPolicy.nominalIntervalForHeuristics : frequency.seconds
}

@MainActor
static func resolvedSourceMode(
provider: UsageProvider,
settings: SettingsStore,
account: ProviderTokenAccount?) -> ProviderSourceMode
{
let base =
ProviderCatalog.implementation(for: provider)?
.sourceMode(context: ProviderSourceModeContext(provider: provider, settings: settings))
?? .auto
let config = settings.configSnapshot.providerConfig(for: provider.instanceID)
return ProviderDescriptorRegistry.descriptor(for: provider).credentials?
.selectedAccountSourceMode(base: base, account: account, config: config) ?? base
}

@MainActor
Expand All @@ -123,12 +143,15 @@ struct ProviderRegistry {
tokenOverride: tokenOverride,
codexActiveSourceOverride: codexActiveSourceOverride)
for implementation in ProviderCatalog.all {
let registration = ProviderDescriptorRegistry.descriptor(for: implementation.id).settingsSection
let registration = ProviderDescriptorRegistry.descriptor(for: implementation.id)
.settingsSection
guard let contribution = implementation.settingsSnapshot(context: context) else {
preconditionFailure("Missing settings snapshot section for provider '\(implementation.id.rawValue)'")
preconditionFailure(
"Missing settings snapshot section for provider '\(implementation.id.rawValue)'")
}
guard registration.accepts(contribution) else {
preconditionFailure("Mismatched settings snapshot section for provider '\(implementation.id.rawValue)'")
preconditionFailure(
"Mismatched settings snapshot section for provider '\(implementation.id.rawValue)'")
}
builder.apply(contribution)
}
Expand Down Expand Up @@ -159,18 +182,26 @@ struct ProviderRegistry {
// Provider-specific by design: managed Codex account selection scopes the fetcher's CODEX_HOME.
if provider == .codex {
let codexActiveSource = codexActiveSourceOverride ?? settings.codexResolvedActiveSource
if let managedHomePath = settings.managedCodexRemoteHomePath(forActiveSource: codexActiveSource) {
if let managedHomePath = settings.managedCodexRemoteHomePath(
forActiveSource: codexActiveSource)
{
env = CodexHomeScope.scopedEnvironment(base: env, codexHome: managedHomePath)
} else if let liveHomePath = settings.liveSystemCodexHomePath(forActiveSource: codexActiveSource) {
} else if let liveHomePath = settings.liveSystemCodexHomePath(
forActiveSource: codexActiveSource)
{
env = CodexHomeScope.scopedEnvironment(base: env, codexHome: liveHomePath)
} else if let profileHomePath = settings.profileCodexHomePath(forActiveSource: codexActiveSource) {
} else if let profileHomePath = settings.profileCodexHomePath(
forActiveSource: codexActiveSource)
{
env = CodexHomeScope.scopedEnvironment(base: env, codexHome: profileHomePath)
}
}
return env
}

static func makeFetcher(base: UsageFetcher, provider: UsageProvider, env: [String: String]) -> UsageFetcher {
static func makeFetcher(base: UsageFetcher, provider: UsageProvider, env: [String: String])
-> UsageFetcher
{
// Provider-specific by design: a Codex account scope needs a fetcher rebuilt with its selected CODEX_HOME.
guard provider == .codex else { return base }
return UsageFetcher(environment: env)
Expand Down
121 changes: 121 additions & 0 deletions Sources/CodexBar/Providers/Grok/GrokProviderImplementation.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,127 @@
import AppKit
import CodexBarCore
import Foundation
import SwiftUI

struct GrokProviderImplementation: ProviderImplementation {
let id: UsageProvider = .grok

@MainActor
func observeSettings(_ settings: SettingsStore) {
_ = settings.grokUsageDataSource
_ = settings.grokCookieSource
_ = settings.grokCookieHeader
}

@MainActor
func sourceMode(context: ProviderSourceModeContext) -> ProviderSourceMode {
context.settings.grokUsageDataSource
}

@MainActor
func openTokenFile(context _: ProviderSettingsContext) -> Bool {
let url = GrokCredentialsStore.tokenFileURLToOpen()
try? FileManager.default.createDirectory(
at: GrokCredentialsStore.grokHomeURL(),
withIntermediateDirectories: true)
NSWorkspace.shared.open(url)
return true
}

@MainActor
func settingsSnapshot(context: ProviderSettingsSnapshotContext)
-> ProviderSettingsSnapshotContribution?
{
.grok(context.settings.grokSettingsSnapshot(tokenOverride: context.tokenOverride))
}

@MainActor
func settingsPickers(context: ProviderSettingsContext) -> [ProviderSettingsPickerDescriptor] {
let sourceBinding = Binding(
get: { context.settings.grokUsageDataSource.rawValue },
set: { raw in
context.settings.grokUsageDataSource = ProviderSourceMode(rawValue: raw) ?? .auto
})
let sourceOptions: [ProviderSettingsPickerOption] = [
ProviderSettingsPickerOption(id: ProviderSourceMode.auto.rawValue, title: "Auto"),
ProviderSettingsPickerOption(id: ProviderSourceMode.cli.rawValue, title: "Grok CLI"),
ProviderSettingsPickerOption(
id: ProviderSourceMode.oauth.rawValue,
title: "SuperGrok OAuth"),
ProviderSettingsPickerOption(
id: ProviderSourceMode.web.rawValue, title: "Browser cookies"),
]
let cookieBinding = Binding(
get: { context.settings.grokCookieSource.rawValue },
set: { raw in
context.settings.grokCookieSource = ProviderCookieSource(rawValue: raw) ?? .auto
})
let cookieOptions = ProviderCookieSourceUI.options(
allowsOff: true,
keychainDisabled: context.settings.debugDisableKeychainAccess)

let cookieSubtitle: () -> String? = {
ProviderCookieSourceUI.subtitle(
source: context.settings.grokCookieSource,
keychainDisabled: context.settings.debugDisableKeychainAccess,
auto: "Automatic imports grok.com cookies from Chrome.",
manual: "Paste a Cookie header from a grok.com request.",
off: "Grok cookies are disabled.")
}

return [
ProviderSettingsPickerDescriptor(
id: "grok-usage-source",
title: "Usage source",
subtitle:
"Auto tries the Grok CLI, SuperGrok OAuth, browser cookies, then bearer gRPC.",
binding: sourceBinding,
options: sourceOptions,
isVisible: nil,
onChange: nil),
ProviderSettingsPickerDescriptor(
id: "grok-cookie-source",
title: "Cookie source",
subtitle: "Automatic imports grok.com cookies from Chrome.",
dynamicSubtitle: cookieSubtitle,
binding: cookieBinding,
options: cookieOptions,
isVisible: {
context.settings.grokUsageDataSource == .auto
|| context.settings.grokUsageDataSource == .web
},
onChange: nil),
]
}

@MainActor
func settingsFields(context: ProviderSettingsContext) -> [ProviderSettingsFieldDescriptor] {
[
ProviderSettingsFieldDescriptor(
id: "grok-cookie",
title: "",
subtitle: "",
kind: .secure,
placeholder: "Cookie: …",
binding: context.stringBinding(\.grokCookieHeader),
actions: [
ProviderSettingsActionDescriptor(
id: "grok-open-usage",
title: "Open grok.com usage",
style: .link,
isVisible: nil,
perform: {
if let url = URL(string: "https://grok.com/?_s=usage") {
NSWorkspace.shared.open(url)
}
}),
],
isVisible: {
(context.settings.grokUsageDataSource == .auto
|| context.settings.grokUsageDataSource == .web)
&& context.settings.grokCookieSource == .manual
},
onActivate: { context.settings.ensureGrokCookieLoaded() }),
]
}
}
56 changes: 56 additions & 0 deletions Sources/CodexBar/Providers/Grok/GrokSettingsStore.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import CodexBarCore
import Foundation

extension SettingsStore {
var grokUsageDataSource: ProviderSourceMode {
get { self.configSnapshot.providerConfig(for: .grok)?.source ?? .auto }
set {
self.updateProviderConfig(provider: .grok) { entry in
entry.source = newValue == .auto ? nil : newValue
}
self.logProviderModeChange(provider: .grok, field: "source", value: newValue.rawValue)
}
}

var grokCookieHeader: String {
get { self.configSnapshot.providerConfig(for: .grok)?.sanitizedCookieHeader ?? "" }
set {
self.updateProviderConfig(provider: .grok) { entry in
entry.cookieHeader = self.normalizedConfigValue(newValue)
}
self.logSecretUpdate(provider: .grok, field: "cookieHeader", value: newValue)
}
}

var grokCookieSource: ProviderCookieSource {
get { self.resolvedCookieSource(provider: .grok, fallback: .auto) }
set {
self.updateProviderConfig(provider: .grok) { entry in
entry.cookieSource = newValue
}
self.logProviderModeChange(
provider: .grok, field: "cookieSource", value: newValue.rawValue)
}
}

func ensureGrokCookieLoaded() {}
}

extension SettingsStore {
func grokSettingsSnapshot(tokenOverride: TokenAccountOverride?)
-> ProviderSettingsSnapshot
.GrokProviderSettings
{
let account = ProviderTokenAccountSelection.selectedAccount(
provider: .grok,
settings: self,
override: tokenOverride)
let resolved = GrokCredentialRouting.cookieSettings(
configuredSource: self.grokCookieSource,
configuredHeader: self.grokCookieHeader,
selectedAccountToken: account?.token)
return GrokProviderSettings(
cookieSource: resolved.cookieSource,
manualCookieHeader: resolved.manualCookieHeader)
}
}
Loading