Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
0c89a8b
Improve DeepSeek detailed usage
Zihao-Qi Jul 13, 2026
6b8b924
Support DeepSeek browser-only usage
Zihao-Qi Jul 13, 2026
bbcf71f
Leave changelog unchanged
Zihao-Qi Jul 16, 2026
d5a09f6
feat: add LongCat usage provider
LeoLin990405 Jun 21, 2026
31f17d1
fix(longcat): surface invalid-session and drop unused today-token path
LeoLin990405 Jun 23, 2026
4f9d9ef
fix(longcat): route env cookie through settings reader
LeoLin990405 Jun 23, 2026
27de94c
fix(longcat): honor cookie source Off/Manual in web strategy
LeoLin990405 Jun 23, 2026
32cd30b
fix(longcat): gate browser cookie import
Jun 24, 2026
a2f0b72
fix(longcat): tighten quota and cookie defaults
Jun 24, 2026
603a5e6
test(longcat): cover HTTP status handling; pin fuel-pack fields
Jul 3, 2026
9011b04
Isolate LongCat HTTP transport
joeVenner Jul 16, 2026
6c49dbe
Add AnyRouter provider
duyet Jul 14, 2026
d85acf1
Simplify AnyRouter provider and switch to black logo
duyet Jul 14, 2026
07b45d2
Distinguish scoped-out AnyRouter keys from rejected ones
duyet Jul 14, 2026
76d90b1
chore: regenerate codex parser hash for CostUsageScanner change
duyet Jul 15, 2026
1964860
style: remove redundant Self in AnyRouterUsageFetcher
duyet Jul 15, 2026
44c80d3
Add ClinePass usage provider
derekszen Jul 15, 2026
cca8875
Add Neuralwatt provider
jrimmer Jun 25, 2026
28d6ed1
fix: harden Neuralwatt provider
steipete Jul 1, 2026
412cceb
fix: avoid duplicate Neuralwatt credits row
steipete Jul 1, 2026
4e7ce28
fix: respect Neuralwatt quota limits
steipete Jul 1, 2026
686b693
Regenerate parser hash
joeVenner Jul 16, 2026
d84f8c8
Fix Neuralwatt account refresh test
joeVenner Jul 16, 2026
37c87b4
Use stored Neuralwatt test accounts
joeVenner Jul 16, 2026
ccd61b0
Narrow Neuralwatt account refresh coverage
joeVenner Jul 16, 2026
c208e4a
Update provider count docs
joeVenner Jul 16, 2026
799f526
Update site provider counts
joeVenner Jul 16, 2026
b1280d3
Split UsageFetcher testing helpers
joeVenner Jul 16, 2026
31ec97c
Stabilize subscription date notes
joeVenner Jul 16, 2026
1d66c62
Stabilize menu card date test helper
joeVenner Jul 16, 2026
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 @@ -3,6 +3,7 @@
## 0.43.1 — Unreleased

### Added
- AnyRouter: add credit balance, lifetime spend, and multi-account API keys from the AnyRouter credits API. Thanks @duyet!
- ZenMux: add Management API usage with five-hour and weekly quotas, subscription expiry, and USD PAYG balance. Thanks @kays0x!

### Fixed
Expand Down
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
[![License: MIT](https://img.shields.io/badge/license-MIT-6e5aff?style=flat-square)](LICENSE)
[![Site](https://img.shields.io/badge/site-codexbar.app-16d3b4?style=flat-square)](https://codexbar.app)

<a href="https://codexbar.app"><img src="docs/social.png" alt="CodexBar — every AI coding limit in your menu bar. 60 providers." width="100%" /></a>
<a href="https://codexbar.app"><img src="docs/social.png" alt="CodexBar — every AI coding limit in your menu bar. 64 providers." width="100%" /></a>

Tiny macOS 14+ menu bar app that keeps **AI coding-provider limits visible** and shows when each window resets. Codex, OpenAI, Claude, Cursor, Gemini, Copilot, Grok, GroqCloud, ElevenLabs, Deepgram, z.ai, MiniMax, Kiro, Zed, Vertex AI, Augment, OpenRouter, LiteLLM, LLM Proxy, Codebuff, Command Code, AWS Bedrock, and many newer coding providers. One status item per provider, or Merge Icons mode with a provider switcher. No Dock icon, minimal UI, dynamic bar icons.
Tiny macOS 14+ menu bar app that keeps **AI coding-provider limits visible** and shows when each window resets. Codex, OpenAI, Claude, Cursor, Gemini, Copilot, Grok, GroqCloud, ElevenLabs, Deepgram, z.ai, MiniMax, Kiro, Zed, Vertex AI, Augment, OpenRouter, LiteLLM, LLM Proxy, Codebuff, Command Code, ClinePass, AWS Bedrock, and many newer coding providers. One status item per provider, or Merge Icons mode with a provider switcher. No Dock icon, minimal UI, dynamic bar icons.

<img src="docs/codexbar.png" alt="CodexBar menu popover with provider tiles, usage bars, and reset countdowns" width="520" />

Expand Down Expand Up @@ -107,6 +107,7 @@ See [CLI configuration](docs/cli-configuration.md) for the full flow.
- [Warp](docs/warp.md) — API token for GraphQL request limits and monthly credits.
- [ElevenLabs](docs/elevenlabs.md) — API key for character credits and voice slot usage.
- [OpenRouter](docs/openrouter.md) — API token for credit-based usage tracking across multiple AI providers.
- [AnyRouter](docs/anyrouter.md) — API key for credit balance and spend across the AnyRouter gateway.
- [CrossModel](docs/crossmodel.md) — API key wallet balance with daily, weekly, and monthly spend.
- [Windsurf](docs/windsurf.md) — Browser localStorage session import or local SQLite cache for plan usage.
- [Zed](docs/zed.md) — Zed editor Keychain session for plan, edit-prediction quota, billing cycle, and overdue invoices.
Expand Down Expand Up @@ -135,6 +136,7 @@ See [CLI configuration](docs/cli-configuration.md) for the full flow.
- [Deepgram](docs/deepgram.md) — API key usage summaries across speech, agent, token, and TTS metrics.
- [Poe](docs/poe.md) — API key for current point balance and recent points history.
- [Chutes](docs/chutes.md) — API key for subscription usage, rolling and monthly quota windows, and pay-as-you-go quotas.
- [Neuralwatt](docs/neuralwatt.md) — API key for USD credit balance and optional per-key spending allowance.
- [ZenMux](docs/zenmux.md) — Management API key for rolling five-hour and seven-day quota windows plus PAYG balance.
- Open to new providers: [provider authoring guide](docs/provider.md).

Expand Down
29 changes: 24 additions & 5 deletions Sources/CodexBar/InlineUsageDashboardContent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -86,10 +86,28 @@ extension UsageMenuCardView.Model {
]
}

if input.provider == .deepseek,
input.showOptionalCreditsAndExtraUsage,
let usage = input.snapshot?.deepseekUsage
{
if input.provider == .deepseek {
if input.isRefreshing {
return []
}
if input.snapshot?.primary == nil {
if input.snapshot?.deepseekDetailedUsageState == .webSessionRequired {
return [L("Sign in to DeepSeek Platform in Chrome for detailed usage.")]
}
if input.snapshot?.deepseekDetailedUsageState == .profileSelectionRequired {
return [L("Select a DeepSeek Chrome profile in Settings.")]
}
}
guard input.showOptionalCreditsAndExtraUsage else { return nil }
guard let usage = input.snapshot?.deepseekUsage else {
if input.snapshot?.deepseekDetailedUsageState == .webSessionRequired {
return [L("Sign in to DeepSeek Platform in Chrome for detailed usage.")]
}
if input.snapshot?.deepseekDetailedUsageState == .profileSelectionRequired {
return [L("Select a DeepSeek Chrome profile in Settings.")]
}
return [L("Detailed usage unavailable.")]
}
let symbol = usage.currency == "CNY" ? "¥" : "$"
let todayCostStr = usage.todayCost.map { "\(symbol)\(String(format: "%.4f", max(0, $0)))" } ?? "—"
return [
Expand Down Expand Up @@ -226,6 +244,7 @@ extension UsageMenuCardView.Model {
return Self.minimaxInlineDashboard(billing)
}
if input.provider == .deepseek,
!input.isRefreshing,
input.showOptionalCreditsAndExtraUsage,
let usage = input.snapshot?.deepseekUsage,
!usage.daily.isEmpty
Expand Down Expand Up @@ -673,7 +692,7 @@ extension UsageMenuCardView.Model {
let monthTokensStr = UsageFormatter.tokenCountString(usage.currentMonthTokens)

return InlineUsageDashboardModel(
accessibilityLabel: L("DeepSeek 30 day token usage trend"),
accessibilityLabel: L("DeepSeek this month token usage trend"),
valueStyle: .tokens,
kpis: [
.init(
Expand Down
2 changes: 1 addition & 1 deletion Sources/CodexBar/MenuCardView+ModelHelpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -380,7 +380,7 @@ extension UsageMenuCardView.Model {

private static func subscriptionDateString(_ date: Date, provider: UsageProvider) -> String {
let formatter = DateFormatter()
formatter.locale = Locale.current
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = self.subscriptionDateTimeZone(provider: provider)
formatter.setLocalizedDateFormatFromTemplate("MMM d, yyyy")
return formatter.string(from: date)
Expand Down
6 changes: 3 additions & 3 deletions Sources/CodexBar/MenuCardView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1280,7 +1280,7 @@ extension UsageMenuCardView.Model {
{
primaryDetailLeft = detail
}
if [.warp, .kilo, .mimo, .deepseek, .qoder, .mistral, .litellm].contains(input.provider),
if [.warp, .kilo, .mimo, .deepseek, .qoder, .mistral, .neuralwatt, .litellm].contains(input.provider),
let detail = primary.resetDescription,
!detail.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
{
Expand Down Expand Up @@ -1309,7 +1309,7 @@ extension UsageMenuCardView.Model {
primaryResetText = nil
}
}
if [.warp, .kilo, .mimo, .deepseek, .qoder, .mistral, .litellm, .zenmux].contains(input.provider),
if [.warp, .kilo, .mimo, .deepseek, .qoder, .mistral, .neuralwatt, .litellm, .zenmux].contains(input.provider),
primary.resetsAt == nil
{
primaryResetText = nil
Expand Down Expand Up @@ -1377,7 +1377,7 @@ extension UsageMenuCardView.Model {
primaryPacePercent = regen.pace.pacePercent
primaryPaceOnTop = regen.pace.paceOnTop
}
let usesBalanceStatusText = input.provider == .deepseek
let usesBalanceStatusText = input.provider == .deepseek || input.provider == .neuralwatt
let primaryStatusText = usesBalanceStatusText ? primaryDetailText : nil
if usesBalanceStatusText {
primaryDetailText = nil
Expand Down
4 changes: 2 additions & 2 deletions Sources/CodexBar/MenuDescriptor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -228,8 +228,8 @@ struct MenuDescriptor {
if let primary = snap.primary {
let primaryDetail = primary.resetDescription?.trimmingCharacters(in: .whitespacesAndNewlines)
let primaryDescriptionIsDetail = provider == .warp || provider == .kilo || provider == .abacus ||
provider == .deepseek || provider == .azureopenai || provider == .mimo || provider == .qoder ||
provider == .sub2api
provider == .deepseek || provider == .neuralwatt || provider == .azureopenai || provider == .mimo ||
provider == .qoder || provider == .sub2api
let primaryWindow = if primaryDescriptionIsDetail {
// Some providers use resetDescription for non-reset detail
// (e.g., "Unlimited", "X/Y credits"). Avoid rendering it as a "Resets ..." line.
Expand Down
22 changes: 19 additions & 3 deletions Sources/CodexBar/PreferencesProviderDetailView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,8 @@ struct ProviderDetailView<SupplementaryContent: View>: View {
provider: self.provider,
model: self.model,
openAIWebDiagnostic: self.openAIWebDiagnostic,
isEnabled: self.isEnabled)
isEnabled: self.isEnabled,
isRefreshing: self.store.refreshingProviders.contains(self.provider))
} header: {
Text(L("Usage"))
}
Expand Down Expand Up @@ -350,6 +351,7 @@ struct ProviderMetricsInlineView: View {
let model: UsageMenuCardView.Model
let openAIWebDiagnostic: String?
let isEnabled: Bool
let isRefreshing: Bool

struct InfoRow: Identifiable, Equatable {
enum ID: Hashable {
Expand Down Expand Up @@ -430,10 +432,24 @@ struct ProviderMetricsInlineView: View {
}

private var placeholderText: String {
if !self.isEnabled {
Self.placeholderText(
isEnabled: self.isEnabled,
isRefreshing: self.isRefreshing,
modelPlaceholder: self.model.placeholder)
}

static func placeholderText(
isEnabled: Bool,
isRefreshing: Bool,
modelPlaceholder: String?) -> String
{
if !isEnabled {
return L("Disabled — no recent data")
}
return self.model.placeholder.map(L) ?? L("No usage yet")
if isRefreshing {
return L("Refreshing")
}
return modelPlaceholder.map(L) ?? L("No usage yet")
}
}

Expand Down
6 changes: 3 additions & 3 deletions Sources/CodexBar/PreferencesProvidersPane.swift
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ struct ProvidersPane: View {
usageText = L("last_fetch_failed")
} else if self.store.knownLimitsAvailability(for: provider)?.isUnavailable == true {
usageText = L("Limits not available")
} else if let snapshot = self.store.snapshot(for: provider) {
} else if let snapshot = self.store.presentationSnapshot(for: provider) {
let relative = snapshot.updatedAt.relativeDescription()
usageText = relative
} else {
Expand All @@ -189,7 +189,7 @@ struct ProvidersPane: View {
L("last_fetch_failed")
} else if self.store.knownLimitsAvailability(for: provider)?.isUnavailable == true {
L("Limits not available")
} else if let snapshot = self.store.snapshot(for: provider) {
} else if let snapshot = self.store.presentationSnapshot(for: provider) {
snapshot.updatedAt.relativeDescription()
} else {
L("usage_not_fetched_yet")
Expand Down Expand Up @@ -620,7 +620,7 @@ struct ProvidersPane: View {

func menuCardModel(for provider: UsageProvider) -> UsageMenuCardView.Model {
let metadata = self.store.metadata(for: provider)
let snapshot = self.store.snapshot(for: provider)
let snapshot = self.store.presentationSnapshot(for: provider)
let now = Date()
let codexProjection = self.store.codexConsumerProjectionIfNeeded(
for: provider,
Expand Down
5 changes: 4 additions & 1 deletion Sources/CodexBar/ProviderRegistry.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,10 @@ struct ProviderRegistry {
runtime: .app,
sourceMode: sourceMode,
includeCredits: false,
includeOptionalUsage: settings.showOptionalCreditsAndExtraUsage,
includeOptionalUsage: ProviderTokenAccountSelection.shouldIncludeOptionalUsage(
provider: provider,
settings: settings,
override: nil),
webTimeout: 60,
webDebugDumpHTML: false,
verbose: verbose,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import CodexBarCore
import Foundation

struct AnyRouterProviderImplementation: ProviderImplementation {
let id: UsageProvider = .anyrouter

@MainActor
func presentation(context _: ProviderPresentationContext) -> ProviderPresentation {
ProviderPresentation { _ in "api" }
}

@MainActor
func observeSettings(_ settings: SettingsStore) {
_ = settings.tokenAccountsData(for: .anyrouter)
}

@MainActor
func isAvailable(context: ProviderAvailabilityContext) -> Bool {
if AnyRouterSettingsReader.apiKey(environment: context.environment) != nil {
return true
}
return !context.settings.tokenAccounts(for: .anyrouter).isEmpty
}

@MainActor
func settingsFields(context _: ProviderSettingsContext) -> [ProviderSettingsFieldDescriptor] {
[]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import CodexBarCore
import Foundation

struct ClinePassProviderImplementation: ProviderImplementation {
let id: UsageProvider = .clinepass

@MainActor
func presentation(context _: ProviderPresentationContext) -> ProviderPresentation {
ProviderPresentation { _ in "api" }
}

@MainActor
func observeSettings(_ settings: SettingsStore) {
_ = settings.clinePassAPIKey
}

@MainActor
func isAvailable(context: ProviderAvailabilityContext) -> Bool {
if ClinePassSettingsReader.apiKey(environment: context.environment) != nil {
return true
}
return !context.settings.clinePassAPIKey.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
}

@MainActor
func settingsFields(context: ProviderSettingsContext) -> [ProviderSettingsFieldDescriptor] {
[
ProviderSettingsFieldDescriptor(
id: "clinepass-api-key",
title: "API key",
subtitle: "Stored in ~/.codexbar/config.json. Paste a ClinePass API key.",
kind: .secure,
placeholder: "ClinePass API key...",
binding: context.stringBinding(\.clinePassAPIKey),
actions: [],
isVisible: nil,
onActivate: nil),
]
}
}
14 changes: 14 additions & 0 deletions Sources/CodexBar/Providers/ClinePass/ClinePassSettingsStore.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import CodexBarCore
import Foundation

extension SettingsStore {
var clinePassAPIKey: String {
get { self.configSnapshot.providerConfig(for: .clinepass)?.sanitizedAPIKey ?? "" }
set {
self.updateProviderConfig(provider: .clinepass) { entry in
entry.apiKey = self.normalizedConfigValue(newValue)
}
self.logSecretUpdate(provider: .clinepass, field: "apiKey", value: newValue)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,23 +1,71 @@
import CodexBarCore
import Foundation
import SwiftUI

struct DeepSeekProviderImplementation: ProviderImplementation {
let id: UsageProvider = .deepseek

@MainActor
func presentation(context _: ProviderPresentationContext) -> ProviderPresentation {
ProviderPresentation { _ in "api" }
ProviderPresentation { context in
context.store.sourceLabel(for: context.provider)
}
}

@MainActor
func observeSettings(_: SettingsStore) {}

@MainActor
func isAvailable(context: ProviderAvailabilityContext) -> Bool {
if DeepSeekSettingsReader.apiKey(environment: context.environment) != nil {
return true
}
return !context.settings.tokenAccounts(for: .deepseek).isEmpty
func settingsPickers(context: ProviderSettingsContext) -> [ProviderSettingsPickerDescriptor] {
let presentationSnapshot = context.store.presentationSnapshot(for: .deepseek)
?? context.store.lastKnownResetSnapshots[.deepseek]
let profiles = presentationSnapshot?.deepseekPlatformProfiles ?? []
guard profiles.count > 1 || presentationSnapshot?.deepseekDetailedUsageState == .profileSelectionRequired
else { return [] }
let apiKey = context.settings.selectedTokenAccount(for: .deepseek)?.token
?? DeepSeekSettingsReader.apiKey(environment: context.store.environmentBase)
let selectedProfileID = context.settings.deepseekProfileID(apiKey: apiKey)
let hasValidSelection = profiles.contains { $0.id == selectedProfileID }
let profileBinding = Binding(
get: {
let profileID = context.settings.deepseekProfileID(apiKey: apiKey)
return profiles.contains { $0.id == profileID } ? profileID : ""
},
set: { profileID in
guard !profileID.isEmpty else { return }
context.store.beginDeepSeekProfileTransition(preservingBalance: apiKey != nil)
context.settings.setDeepSeekProfileID(profileID, apiKey: apiKey)
})
let options = (hasValidSelection
? []
: [ProviderSettingsPickerOption(id: "", title: "Select profile…")])
+ profiles.map { ProviderSettingsPickerOption(id: $0.id, title: $0.name) }

return [
ProviderSettingsPickerDescriptor(
id: "deepseek-chrome-profile",
title: "Chrome profile",
subtitle: "Choose which signed-in DeepSeek Platform session supplies detailed usage.",
dynamicSubtitle: {
context.store.refreshingProviders.contains(.deepseek)
? "Refreshing"
: nil
},
binding: profileBinding,
options: options,
isVisible: nil,
isEnabled: { !context.store.refreshingProviders.contains(.deepseek) },
onChange: { _ in
await ProviderInteractionContext.$current.withValue(.userInitiated) {
await context.store.refreshProvider(.deepseek, allowDisabled: true)
}
}),
]
}

@MainActor
func isAvailable(context _: ProviderAvailabilityContext) -> Bool {
true
}

@MainActor
Expand Down
Loading