Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
4343e47
Add local session cost ledger
wicolian Jul 14, 2026
c687c8f
Namespace status item autosave names
wicolian Jul 14, 2026
4c3f7c8
Clarify Codex local API-key cost ledger
wicolian Jul 14, 2026
433ea2a
Add provider-specific confetti palettes
kreitter Jul 12, 2026
ce3045c
Add confetti palette preferences
kreitter Jul 14, 2026
90304e5
Preserve invalid confetti palette drafts
kreitter Jul 15, 2026
ea143af
Clarify confetti palette input state
kreitter Jul 16, 2026
83f839e
Add confetti palette preview
kreitter Jul 16, 2026
7c13764
Simplify confetti palette editing
kreitter Jul 16, 2026
4d84071
Merge remote-tracking branch 'origin/main' into provider-confetti-pal…
steipete Jul 17, 2026
f5f638d
Merge remote-tracking branch 'origin/main' into codex/local-codex-ses…
steipete Jul 17, 2026
57eabca
fix: refresh visible session cost details
steipete Jul 17, 2026
4cc44da
fix: harden session cost details
steipete Jul 17, 2026
e7c94b9
feat: ship provider confetti defaults
steipete Jul 17, 2026
922bf06
Merge remote-tracking branch 'origin/main' into provider-confetti-pal…
steipete Jul 17, 2026
8557f81
fix: scope local session cost rows
steipete Jul 17, 2026
7348d3d
fix: enforce Codex cost scope boundaries
steipete Jul 17, 2026
b11a4ee
fix: isolate managed Codex cost sources
steipete Jul 17, 2026
cd032ea
Merge remote-tracking branch 'origin/main' into codex/local-codex-ses…
steipete Jul 17, 2026
d9b4d03
Merge remote-tracking branch 'origin/main' into provider-confetti-pal…
steipete Jul 17, 2026
ad170dc
feat: add ClinePass confetti palette
steipete Jul 17, 2026
c30750b
Merge remote-tracking branch 'origin/main' into codex/local-codex-ses…
steipete Jul 17, 2026
a2a21aa
Merge remote-tracking branch 'origin/main' into provider-confetti-pal…
steipete Jul 17, 2026
7c58c94
feat: add LongCat confetti palette
steipete Jul 17, 2026
7540b5d
test: split provider settings descriptor tests
steipete Jul 17, 2026
22eddb3
Merge remote-tracking branch 'origin/main' into codex/local-codex-ses…
steipete Jul 17, 2026
d6d75ee
Merge remote-tracking branch 'origin/main' into codex/local-codex-ses…
steipete Jul 17, 2026
b97a155
Merge remote-tracking branch 'origin/main' into provider-confetti-pal…
steipete Jul 17, 2026
6321d01
Merge remote-tracking branch 'origin/main' into codex/local-codex-ses…
steipete Jul 17, 2026
0f27d0e
Merge pull request #2177 from kreitter/provider-confetti-palettes-cur…
steipete Jul 17, 2026
63215da
Merge pull request #2172 from wicolian/codex/local-codex-session-costs
steipete Jul 17, 2026
c712db6
feat: add ai& spend provider
jethac Jul 17, 2026
7b5bf90
feat: render ai& spend through the shared cost card
jethac Jul 17, 2026
927697e
docs: document the ai& provider
jethac Jul 17, 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
- ai&: add 30-day organization spend from the documented analytics summary API using org-scoped API keys.
- ZenMux: add Management API usage with five-hour and weekly quotas, subscription expiry, and USD PAYG balance. Thanks @kays0x!

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
[![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. 61 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, 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.

Expand Down
3 changes: 2 additions & 1 deletion Sources/CodexBar/CodexbarApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
resetKind: String)
{
let origin = self.statusController?.celebrationOriginPoint(for: provider)
let palette = ProviderDescriptorRegistry.descriptor(for: provider).branding.confettiPalette
self.confettiLogger.info(
"Triggering confetti",
metadata: [
Expand All @@ -458,7 +459,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
"resetKind": resetKind,
"originKnown": origin == nil ? "0" : "1",
])
self.confettiOverlayController.play(originInScreen: origin)
self.confettiOverlayController.play(originInScreen: origin, colors: palette)
}

/// Use the classic (non-Liquid Glass) app icon on macOS versions before 26.
Expand Down
127 changes: 127 additions & 0 deletions Sources/CodexBar/CostHistoryChartMenuView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ struct CostHistoryChartMenuView: View {
private let historyDays: Int
private let windowLabel: String?
private let projects: [CostUsageProjectBreakdown]
private let sessions: [CostUsageSessionBreakdown]
private let width: CGFloat
@State private var selectedDateKey: String?

Expand All @@ -59,6 +60,7 @@ struct CostHistoryChartMenuView: View {
historyDays: Int = 30,
windowLabel: String? = nil,
projects: [CostUsageProjectBreakdown] = [],
sessions: [CostUsageSessionBreakdown] = [],
width: CGFloat)
{
self.provider = provider
Expand All @@ -68,6 +70,7 @@ struct CostHistoryChartMenuView: View {
self.historyDays = max(1, min(365, historyDays))
self.windowLabel = windowLabel
self.projects = projects
self.sessions = sessions
self.width = width
}

Expand Down Expand Up @@ -284,6 +287,10 @@ struct CostHistoryChartMenuView: View {
}
.frame(height: Self.projectBlockHeight(projects: self.projects), alignment: .topLeading)
}

if !self.sessions.isEmpty {
self.sessionsBlock
}
}
.padding(.horizontal, 16)
.padding(.vertical, Self.verticalPadding)
Expand Down Expand Up @@ -327,8 +334,95 @@ struct CostHistoryChartMenuView: View {
private static let projectSourceIndent: CGFloat = 10
private static let projectMoreRowHeight: CGFloat = 16
private static let maxVisibleProjectSourceRows = 2
private static let sessionRowHeight: CGFloat = 44
private static let sessionRowSpacing: CGFloat = 5
private static let maxVisibleSessionRows = 5
static let verticalPadding: CGFloat = 10

private var sessionsBlock: some View {
let visibleCount = min(self.sessions.count, Self.maxVisibleSessionRows)
return VStack(alignment: .leading, spacing: Self.sessionRowSpacing) {
HStack {
Text("Conversations (\(self.windowLabel ?? Self.windowLabel(days: self.historyDays)))")
.font(.caption)
.foregroundStyle(.secondary)
.lineLimit(1)
Spacer()
Text("\(self.sessions.count)")
.font(.caption2)
.foregroundStyle(Color(nsColor: .tertiaryLabelColor))
}
.frame(height: Self.detailPrimaryLineHeight, alignment: .leading)

ScrollView(.vertical) {
LazyVStack(alignment: .leading, spacing: Self.sessionRowSpacing) {
ForEach(self.sessions) { session in
self.sessionRow(session)
}
}
}
.scrollIndicators(self.sessions.count > visibleCount ? .visible : .hidden)
.frame(
height: CGFloat(visibleCount) * Self.sessionRowHeight
+ CGFloat(max(visibleCount - 1, 0)) * Self.sessionRowSpacing,
alignment: .topLeading)
}
.frame(
height: Self.detailPrimaryLineHeight + Self.sessionRowSpacing
+ CGFloat(visibleCount) * Self.sessionRowHeight
+ CGFloat(max(visibleCount - 1, 0)) * Self.sessionRowSpacing,
alignment: .topLeading)
}

private func sessionRow(_ session: CostUsageSessionBreakdown) -> some View {
HStack(alignment: .top, spacing: 8) {
VStack(alignment: .leading, spacing: 1) {
Text("Session \(Self.shortSessionID(session.sessionID))")
.font(.caption)
.foregroundStyle(.secondary)
.lineLimit(1)
Text(Self.sessionUsageLine(session))
.font(.caption2)
.foregroundStyle(Color(nsColor: .tertiaryLabelColor))
.lineLimit(1)
.truncationMode(.tail)
Text(session.lastActivity, format: .dateTime.month(.abbreviated).day().hour().minute())
.font(.caption2)
.foregroundStyle(Color(nsColor: .tertiaryLabelColor))
.lineLimit(1)
}
Spacer(minLength: 8)
Text(session.costUSD.map(self.costString) ?? "—")
.font(.caption)
.monospacedDigit()
.foregroundStyle(.secondary)
.lineLimit(1)
}
.frame(height: Self.sessionRowHeight, alignment: .topLeading)
.accessibilityElement(children: .combine)
}

static func shortSessionID(_ sessionID: String) -> String {
let trimmed = sessionID.trimmingCharacters(in: .whitespacesAndNewlines)
guard trimmed.count > 12 else { return trimmed }
return "\(trimmed.prefix(4))...\(trimmed.suffix(8))"
}

private static func sessionUsageLine(_ session: CostUsageSessionBreakdown) -> String {
let models = session.modelBreakdowns.map(\.modelName)
let modelLabel = if models.isEmpty {
"Unknown model"
} else if models.count == 1 {
models[0]
} else {
"\(models[0]) +\(models.count - 1)"
}
let input = session.inputTokens.map(UsageFormatter.tokenCountString) ?? "—"
let cached = session.cachedInputTokens.map(UsageFormatter.tokenCountString) ?? "—"
let output = session.outputTokens.map(UsageFormatter.tokenCountString) ?? "—"
return "\(modelLabel) · \(input) input · \(cached) cached · \(output) output"
}

static func windowLabel(days: Int) -> String {
if days == 1 {
return L("Today")
Expand Down Expand Up @@ -788,6 +882,7 @@ extension CostHistoryChartMenuView {
let hasDailyEntries: Bool
let daily: [VisibleDailyFingerprint]
let projects: [VisibleProjectFingerprint]
let sessions: [VisibleSessionFingerprint]
}

struct VisibleDailyFingerprint: Equatable {
Expand Down Expand Up @@ -824,11 +919,23 @@ extension CostHistoryChartMenuView {
let totalCostBitPattern: UInt64?
}

struct VisibleSessionFingerprint: Equatable {
let sessionID: String
let lastActivityBitPattern: UInt64
let inputTokens: Int?
let cachedInputTokens: Int?
let outputTokens: Int?
let totalTokens: Int?
let costBitPattern: UInt64?
let models: [VisibleModelBreakdownFingerprint]
}

static func renderFingerprint(
from snapshot: CostUsageTokenSnapshot,
provider: UsageProvider) -> RenderFingerprint
{
let projects = provider == .codex ? snapshot.projects : []
let sessions = provider == .codex ? snapshot.sessions : []
return RenderFingerprint(
currencyCode: snapshot.currencyCode,
historyDays: snapshot.historyDays,
Expand All @@ -854,6 +961,26 @@ extension CostHistoryChartMenuView {
totalTokens: source.totalTokens,
totalCostBitPattern: source.totalCostUSD.map(\.bitPattern))
})
},
sessions: sessions.map { session in
VisibleSessionFingerprint(
sessionID: session.sessionID,
lastActivityBitPattern: session.lastActivity.timeIntervalSince1970.bitPattern,
inputTokens: session.inputTokens,
cachedInputTokens: session.cachedInputTokens,
outputTokens: session.outputTokens,
totalTokens: session.totalTokens,
costBitPattern: session.costUSD.map(\.bitPattern),
models: session.modelBreakdowns.map { item in
VisibleModelBreakdownFingerprint(
modelName: item.modelName,
costBitPattern: item.costUSD.map(\.bitPattern),
totalTokens: item.totalTokens,
standardCostBitPattern: item.standardCostUSD.map(\.bitPattern),
priorityCostBitPattern: item.priorityCostUSD.map(\.bitPattern),
standardTokens: item.standardCostUSD == nil ? nil : item.standardTokens,
priorityTokens: item.priorityCostUSD == nil ? nil : item.priorityTokens)
})
})
}

Expand Down
4 changes: 3 additions & 1 deletion Sources/CodexBar/MenuCardView+Costs.swift
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,9 @@ extension UsageMenuCardView.Model {
percentLine: nil)
}

if provider == .openai || provider == .claude || provider == .litellm, cost.limit <= 0 {
if provider == .openai || provider == .claude || provider == .litellm || provider == .aiand,
cost.limit <= 0
{
let spend = UsageFormatter.currencyString(cost.used, currencyCode: cost.currencyCode)
let periodLabel = Self.localizedPeriodLabel(cost.period ?? "Last 30 days")
return ProviderCostSection(
Expand Down
13 changes: 13 additions & 0 deletions Sources/CodexBar/MenuCardView+ModelHelpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,15 @@ extension UsageMenuCardView.Model {
else {
return nil
}
// Local Codex session costs are independent from OAuth, CLI quota, and OpenAI web
// dashboard access. Do not present a failed account-level quota fetch as a failure of
// a valid local API-key ledger.
if input.codexLocalSessionCostLedgerEnabled,
self.hasLocalCodexTokenUsage(input),
self.isRemoteCodexQuotaFetchError(lastError)
{
return nil
}
if self.shouldShowRateLimitsUnavailablePlaceholder(input: input, lastError: lastError) {
return nil
}
Expand Down Expand Up @@ -402,6 +411,10 @@ extension UsageMenuCardView.Model {
self.tokenUsageSnapshot(input: input) != nil
}

private static func isRemoteCodexQuotaFetchError(_ error: String) -> Bool {
error.localizedCaseInsensitiveContains("Codex usage is temporarily unavailable")
}

private static func shouldShowRateLimitsUnavailablePlaceholder(input: Input, lastError: String? = nil) -> Bool {
let currentError = lastError ?? input.lastError
if let currentError = currentError?.trimmingCharacters(in: .whitespacesAndNewlines),
Expand Down
3 changes: 3 additions & 0 deletions Sources/CodexBar/MenuCardView+ModelInput.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ extension UsageMenuCardView.Model {
let usageBarsShowUsed: Bool
let resetTimeDisplayStyle: ResetTimeDisplayStyle
let tokenCostUsageEnabled: Bool
let codexLocalSessionCostLedgerEnabled: Bool
let tokenCostInlineDashboardEnabled: Bool
let tokenCostMenuSectionEnabled: Bool
let costComparisonPeriodsEnabled: Bool
Expand Down Expand Up @@ -57,6 +58,7 @@ extension UsageMenuCardView.Model {
usageBarsShowUsed: Bool,
resetTimeDisplayStyle: ResetTimeDisplayStyle,
tokenCostUsageEnabled: Bool,
codexLocalSessionCostLedgerEnabled: Bool = false,
tokenCostInlineDashboardEnabled: Bool? = nil,
tokenCostMenuSectionEnabled: Bool? = nil,
costComparisonPeriodsEnabled: Bool = false,
Expand Down Expand Up @@ -91,6 +93,7 @@ extension UsageMenuCardView.Model {
self.usageBarsShowUsed = usageBarsShowUsed
self.resetTimeDisplayStyle = resetTimeDisplayStyle
self.tokenCostUsageEnabled = tokenCostUsageEnabled
self.codexLocalSessionCostLedgerEnabled = codexLocalSessionCostLedgerEnabled
self.tokenCostInlineDashboardEnabled = tokenCostInlineDashboardEnabled ?? tokenCostUsageEnabled
self.tokenCostMenuSectionEnabled = tokenCostMenuSectionEnabled ?? tokenCostUsageEnabled
self.costComparisonPeriodsEnabled = costComparisonPeriodsEnabled
Expand Down
5 changes: 4 additions & 1 deletion Sources/CodexBar/PreferencesProvidersPane.swift
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,9 @@ struct ProvidersPane: View {
isPresented: Binding(
get: { self.activeConfirmation != nil },
set: { isPresented in
if !isPresented { self.activeConfirmation = nil }
if !isPresented {
self.activeConfirmation = nil
}
}),
actions: {
if let active = self.activeConfirmation {
Expand Down Expand Up @@ -684,6 +686,7 @@ struct ProvidersPane: View {
usageBarsShowUsed: self.settings.usageBarsShowUsed,
resetTimeDisplayStyle: self.settings.resetTimeDisplayStyle,
tokenCostUsageEnabled: self.settings.isCostUsageEffectivelyEnabled(for: provider),
codexLocalSessionCostLedgerEnabled: self.settings.codexLocalSessionCostLedgerEnabled,
tokenCostInlineDashboardEnabled: self.settings.costSummaryShowsInlineDashboard(for: provider),
// Display style only controls the main menu. Provider details always expose
// available cost data in their Usage section.
Expand Down
52 changes: 52 additions & 0 deletions Sources/CodexBar/Providers/AiAnd/AiAndProviderImplementation.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import AppKit
import CodexBarCore
import Foundation

struct AiAndProviderImplementation: ProviderImplementation {
let id: UsageProvider = .aiand

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

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

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

@MainActor
func settingsFields(context: ProviderSettingsContext) -> [ProviderSettingsFieldDescriptor] {
[
ProviderSettingsFieldDescriptor(
id: "aiand-api-key",
title: "API key",
subtitle: "Stored in CodexBar's config file. Create a key in the ai& console (shown once).",
kind: .secure,
placeholder: "sk-…",
binding: context.stringBinding(\.aiAndAPIKey),
actions: [
ProviderSettingsActionDescriptor(
id: "aiand-open-console",
title: "Open ai& Console",
style: .link,
isVisible: nil,
perform: {
if let url = URL(string: "https://console.aiand.com") {
NSWorkspace.shared.open(url)
}
}),
],
isVisible: nil,
onActivate: nil),
]
}
}
14 changes: 14 additions & 0 deletions Sources/CodexBar/Providers/AiAnd/AiAndSettingsStore.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import CodexBarCore
import Foundation

extension SettingsStore {
var aiAndAPIKey: String {
get { self.configSnapshot.providerConfig(for: .aiand)?.sanitizedAPIKey ?? "" }
set {
self.updateProviderConfig(provider: .aiand) { entry in
entry.apiKey = self.normalizedConfigValue(newValue)
}
self.logSecretUpdate(provider: .aiand, field: "apiKey", value: newValue)
}
}
}
Loading