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 @@ -4,6 +4,7 @@

### Added

- Pi: add standalone local Pi/OMP token and estimated-cost history across the app, CLI, Overview, Usage & Spend, and widgets, with scoped cache recovery and source accounting that prevents duplicate Claude/Codex totals (#3246). Thanks @Yuxin-Qiao!
- Dashboard: include provider-reported 30-day USD spend when no local cost row exists, preserving OpenRouter's completed UTC history without inventing a local Today total (#3748). Thanks @Chipagosfinest!
- Widgets: select DeepSeek and OpenRouter, see their balances, and keep live update ages visible in small widgets (#3743). Thanks @brzvsk!

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,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. 74 providers." width="100%" /></a>
<a href="https://codexbar.app"><img src="docs/social.png" alt="CodexBar — every AI coding limit in your menu bar. 75 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
1 change: 1 addition & 0 deletions Sources/CodexBar/LoadingPattern.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ enum LoadingPattern: String, CaseIterable, Identifiable {
}

/// Secondary offset so the lower bar moves differently.
/// Provider-specific by design: these are mathematical phase constants, not provider cases.
var secondaryOffset: Double {
switch self {
case .knightRider: .pi
Expand Down
24 changes: 19 additions & 5 deletions Sources/CodexBar/PreferencesSpendDashboardPane.swift
Original file line number Diff line number Diff line change
Expand Up @@ -568,7 +568,7 @@ struct SpendDashboardCurrencySection: View {
value: UsageFormatter.currencyString(metered, currencyCode: self.group.currencyCode))
}
SpendSummaryValue(
title: L("Subscriptions"),
title: spendDashboardProviderCountTitle(self.group),
value: codexBarLocalizedInteger(self.group.providers.count))
Spacer()
}
Expand Down Expand Up @@ -643,7 +643,7 @@ private struct SpendProviderPanel: View {
var body: some View {
SpendDashboardPanel {
VStack(alignment: .leading, spacing: 0) {
Text(L("By subscription")).font(.headline).padding(.bottom, 8)
Text(spendDashboardProviderPanelTitle(self.group)).font(.headline).padding(.bottom, 8)
ForEach(self.group.providers) { row in
if row.rank > 1 {
Divider()
Expand Down Expand Up @@ -1487,8 +1487,22 @@ func spendDashboardGroupTokenText(_ group: SpendDashboardModel.CurrencyGroup) ->
return group.hasPartialTokens ? "~\(formatted)" : formatted
}

func spendDashboardPartialSubscriptionsText(_ group: SpendDashboardModel.CurrencyGroup) -> String {
L("%d of %d subscriptions have spend", group.pricedProviderCount, group.providers.count)
private func spendDashboardIncludesLocalHistory(_ group: SpendDashboardModel.CurrencyGroup) -> Bool {
group.providers.contains { $0.sourceKind == .localHistory }
}

func spendDashboardProviderCountTitle(_ group: SpendDashboardModel.CurrencyGroup) -> String {
spendDashboardIncludesLocalHistory(group) ? L("Sources") : L("Subscriptions")
}

func spendDashboardProviderPanelTitle(_ group: SpendDashboardModel.CurrencyGroup) -> String {
spendDashboardIncludesLocalHistory(group) ? L("By source") : L("By subscription")
}

func spendDashboardPartialSourceCoverageText(_ group: SpendDashboardModel.CurrencyGroup) -> String {
let template = spendDashboardIncludesLocalHistory(group)
? "%d of %d sources have spend" : "%d of %d subscriptions have spend"
return L(template, group.pricedProviderCount, group.providers.count)
}

func spendDashboardHistoryCaption(
Expand All @@ -1499,7 +1513,7 @@ func spendDashboardHistoryCaption(
if group.hasPartialCost || group.hasPartialTokens {
parts.append(L("Partial estimate"))
if group.hasPartialCost {
parts.append(spendDashboardPartialSubscriptionsText(group))
parts.append(spendDashboardPartialSourceCoverageText(group))
}
} else {
parts.append(L("Local estimated history"))
Expand Down
6 changes: 6 additions & 0 deletions Sources/CodexBar/Providers/Pi/PiProviderImplementation.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import CodexBarCore
import Foundation

struct PiProviderImplementation: ProviderImplementation {
let id: UsageProvider = .pi
}
Original file line number Diff line number Diff line change
Expand Up @@ -79,5 +79,6 @@ enum ProviderImplementationManifest {
CodeRabbitProviderImplementation(),
ReplicateProviderImplementation(),
HuggingFaceProviderImplementation(),
PiProviderImplementation(),
]
}
3 changes: 3 additions & 0 deletions Sources/CodexBar/Resources/ProviderIcon-pi.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions Sources/CodexBar/Resources/ar.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1352,6 +1352,8 @@
"Tracked tokens" = "الرموز المتتبعة";
"Subscriptions" = "الاشتراكات";
"By subscription" = "حسب الاشتراك";
"By source" = "By source";
"%d of %d sources have spend" = "%d of %d sources have spend";
"No model-level history" = "لا يوجد سجل على مستوى النموذج";
"Daily estimated spend" = "الإنفاق اليومي التقديري";
"Hourly estimated spend" = "الإنفاق الساعي التقديري";
Expand Down
2 changes: 2 additions & 0 deletions Sources/CodexBar/Resources/ca.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1351,6 +1351,8 @@
"Tracked tokens" = "Tokens registrats";
"Subscriptions" = "Subscripcions";
"By subscription" = "Per subscripció";
"By source" = "By source";
"%d of %d sources have spend" = "%d of %d sources have spend";
"No model-level history" = "Sense historial per model";
"Daily estimated spend" = "Despesa diària estimada";
"Hourly estimated spend" = "Despesa horària estimada";
Expand Down
2 changes: 2 additions & 0 deletions Sources/CodexBar/Resources/de.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1349,6 +1349,8 @@
"Tracked tokens" = "Erfasste Token";
"Subscriptions" = "Abonnements";
"By subscription" = "Nach Abonnement";
"By source" = "By source";
"%d of %d sources have spend" = "%d of %d sources have spend";
"No model-level history" = "Kein Verlauf auf Modellebene";
"Daily estimated spend" = "Geschätzte tägliche Ausgaben";
"Hourly estimated spend" = "Geschätzte stündliche Ausgaben";
Expand Down
2 changes: 2 additions & 0 deletions Sources/CodexBar/Resources/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1336,6 +1336,8 @@
"Tracked tokens" = "Tracked tokens";
"Subscriptions" = "Subscriptions";
"By subscription" = "By subscription";
"By source" = "By source";
"%d of %d sources have spend" = "%d of %d sources have spend";
"No model-level history" = "No model-level history";
"Daily estimated spend" = "Daily estimated spend";
"Hourly estimated spend" = "Hourly estimated spend";
Expand Down
2 changes: 2 additions & 0 deletions Sources/CodexBar/Resources/es.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1347,6 +1347,8 @@
"Tracked tokens" = "Tokens registrados";
"Subscriptions" = "Suscripciones";
"By subscription" = "Por suscripción";
"By source" = "By source";
"%d of %d sources have spend" = "%d of %d sources have spend";
"No model-level history" = "No hay historial por modelo";
"Daily estimated spend" = "Gasto diario estimado";
"Hourly estimated spend" = "Gasto horario estimado";
Expand Down
2 changes: 2 additions & 0 deletions Sources/CodexBar/Resources/fa.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1352,6 +1352,8 @@
"Tracked tokens" = "توکن‌های پیگیری‌شده";
"Subscriptions" = "اشتراک‌ها";
"By subscription" = "بر اساس اشتراک";
"By source" = "By source";
"%d of %d sources have spend" = "%d of %d sources have spend";
"No model-level history" = "تاریخچه‌ای در سطح مدل وجود ندارد";
"Daily estimated spend" = "برآورد هزینه روزانه";
"Hourly estimated spend" = "برآورد هزینه ساعتی";
Expand Down
2 changes: 2 additions & 0 deletions Sources/CodexBar/Resources/fr.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1348,6 +1348,8 @@
"Tracked tokens" = "Jetons suivis";
"Subscriptions" = "Abonnements";
"By subscription" = "Par abonnement";
"By source" = "By source";
"%d of %d sources have spend" = "%d of %d sources have spend";
"No model-level history" = "Aucun historique au niveau des modèles";
"Daily estimated spend" = "Dépenses quotidiennes estimées";
"Hourly estimated spend" = "Dépenses horaires estimées";
Expand Down
2 changes: 2 additions & 0 deletions Sources/CodexBar/Resources/gl.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1348,6 +1348,8 @@
"Tracked tokens" = "Tokens rexistrados";
"Subscriptions" = "Subscricións";
"By subscription" = "Por subscrición";
"By source" = "By source";
"%d of %d sources have spend" = "%d of %d sources have spend";
"No model-level history" = "Sen historial por modelo";
"Daily estimated spend" = "Gasto diario estimado";
"Hourly estimated spend" = "Gasto horario estimado";
Expand Down
2 changes: 2 additions & 0 deletions Sources/CodexBar/Resources/id.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1352,6 +1352,8 @@
"Tracked tokens" = "Token yang dilacak";
"Subscriptions" = "Langganan";
"By subscription" = "Berdasarkan langganan";
"By source" = "By source";
"%d of %d sources have spend" = "%d of %d sources have spend";
"No model-level history" = "Tidak ada riwayat tingkat model";
"Daily estimated spend" = "Perkiraan pengeluaran harian";
"Hourly estimated spend" = "Perkiraan pengeluaran per jam";
Expand Down
2 changes: 2 additions & 0 deletions Sources/CodexBar/Resources/it.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1352,6 +1352,8 @@
"Tracked tokens" = "Token tracciati";
"Subscriptions" = "Abbonamenti";
"By subscription" = "Per abbonamento";
"By source" = "Per fonte";
"%d of %d sources have spend" = "%d di %d fonti hanno dati di spesa";
"No model-level history" = "Nessuna cronologia a livello di modello";
"Daily estimated spend" = "Spesa giornaliera stimata";
"Hourly estimated spend" = "Spesa oraria stimata";
Expand Down
2 changes: 2 additions & 0 deletions Sources/CodexBar/Resources/ja.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1349,6 +1349,8 @@
"Tracked tokens" = "追跡対象トークン";
"Subscriptions" = "サブスクリプション";
"By subscription" = "サブスクリプション別";
"By source" = "By source";
"%d of %d sources have spend" = "%d of %d sources have spend";
"No model-level history" = "モデル別の履歴はありません";
"Daily estimated spend" = "日別推定支出";
"Hourly estimated spend" = "時間別推定支出";
Expand Down
2 changes: 2 additions & 0 deletions Sources/CodexBar/Resources/ko.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1318,6 +1318,8 @@
"Tracked tokens" = "추적된 토큰";
"Subscriptions" = "구독";
"By subscription" = "구독별";
"By source" = "By source";
"%d of %d sources have spend" = "%d of %d sources have spend";
"No model-level history" = "모델별 내역이 없습니다";
"Daily estimated spend" = "일별 예상 지출";
"Hourly estimated spend" = "시간별 예상 지출";
Expand Down
2 changes: 2 additions & 0 deletions Sources/CodexBar/Resources/nl.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1348,6 +1348,8 @@
"Tracked tokens" = "Bijgehouden tokens";
"Subscriptions" = "Abonnementen";
"By subscription" = "Per abonnement";
"By source" = "By source";
"%d of %d sources have spend" = "%d of %d sources have spend";
"No model-level history" = "Geen geschiedenis op modelniveau";
"Daily estimated spend" = "Geschatte dagelijkse uitgaven";
"Hourly estimated spend" = "Geschatte uitgaven per uur";
Expand Down
2 changes: 2 additions & 0 deletions Sources/CodexBar/Resources/pl.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1352,6 +1352,8 @@
"Tracked tokens" = "Śledzone tokeny";
"Subscriptions" = "Subskrypcje";
"By subscription" = "Według subskrypcji";
"By source" = "By source";
"%d of %d sources have spend" = "%d of %d sources have spend";
"No model-level history" = "Brak historii na poziomie modeli";
"Daily estimated spend" = "Szacowane dzienne wydatki";
"Hourly estimated spend" = "Szacowane wydatki godzinowe";
Expand Down
2 changes: 2 additions & 0 deletions Sources/CodexBar/Resources/pt-BR.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1349,6 +1349,8 @@
"Tracked tokens" = "Tokens acompanhados";
"Subscriptions" = "Assinaturas";
"By subscription" = "Por assinatura";
"By source" = "By source";
"%d of %d sources have spend" = "%d of %d sources have spend";
"No model-level history" = "Sem histórico por modelo";
"Daily estimated spend" = "Gasto diário estimado";
"Hourly estimated spend" = "Gasto horário estimado";
Expand Down
2 changes: 2 additions & 0 deletions Sources/CodexBar/Resources/ru.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1350,6 +1350,8 @@
"Tracked tokens" = "Отслеживаемые токены";
"Subscriptions" = "Подписки";
"By subscription" = "По подпискам";
"By source" = "By source";
"%d of %d sources have spend" = "%d of %d sources have spend";
"No model-level history" = "Нет истории по моделям";
"Daily estimated spend" = "Предполагаемые ежедневные расходы";
"Hourly estimated spend" = "Предполагаемые почасовые расходы";
Expand Down
2 changes: 2 additions & 0 deletions Sources/CodexBar/Resources/sv.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1347,6 +1347,8 @@
"Tracked tokens" = "Spårade token";
"Subscriptions" = "Abonnemang";
"By subscription" = "Per abonnemang";
"By source" = "By source";
"%d of %d sources have spend" = "%d of %d sources have spend";
"No model-level history" = "Ingen historik på modellnivå";
"Daily estimated spend" = "Uppskattade dagliga utgifter";
"Hourly estimated spend" = "Uppskattade utgifter per timme";
Expand Down
2 changes: 2 additions & 0 deletions Sources/CodexBar/Resources/th.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1352,6 +1352,8 @@
"Tracked tokens" = "โทเค็นที่ติดตาม";
"Subscriptions" = "การสมัครสมาชิก";
"By subscription" = "แยกตามการสมัครสมาชิก";
"By source" = "By source";
"%d of %d sources have spend" = "%d of %d sources have spend";
"No model-level history" = "ไม่มีประวัติระดับโมเดล";
"Daily estimated spend" = "ค่าใช้จ่ายรายวันโดยประมาณ";
"Hourly estimated spend" = "ค่าใช้จ่ายรายชั่วโมงโดยประมาณ";
Expand Down
2 changes: 2 additions & 0 deletions Sources/CodexBar/Resources/tr.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1350,6 +1350,8 @@
"Tracked tokens" = "İzlenen tokenlar";
"Subscriptions" = "Abonelikler";
"By subscription" = "Aboneliğe göre";
"By source" = "By source";
"%d of %d sources have spend" = "%d of %d sources have spend";
"No model-level history" = "Model düzeyinde geçmiş yok";
"Daily estimated spend" = "Günlük tahmini harcama";
"Hourly estimated spend" = "Saatlik tahmini harcama";
Expand Down
2 changes: 2 additions & 0 deletions Sources/CodexBar/Resources/uk.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1348,6 +1348,8 @@
"Tracked tokens" = "Відстежувані токени";
"Subscriptions" = "Підписки";
"By subscription" = "За підписками";
"By source" = "By source";
"%d of %d sources have spend" = "%d of %d sources have spend";
"No model-level history" = "Немає історії за моделями";
"Daily estimated spend" = "Орієнтовні щоденні витрати";
"Hourly estimated spend" = "Орієнтовні погодинні витрати";
Expand Down
2 changes: 2 additions & 0 deletions Sources/CodexBar/Resources/vi.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1349,6 +1349,8 @@
"Tracked tokens" = "Token được theo dõi";
"Subscriptions" = "Gói đăng ký";
"By subscription" = "Theo gói đăng ký";
"By source" = "By source";
"%d of %d sources have spend" = "%d of %d sources have spend";
"No model-level history" = "Không có lịch sử theo mô hình";
"Daily estimated spend" = "Chi tiêu ước tính hằng ngày";
"Hourly estimated spend" = "Chi tiêu ước tính theo giờ";
Expand Down
2 changes: 2 additions & 0 deletions Sources/CodexBar/Resources/zh-Hans.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1331,6 +1331,8 @@
"Tracked tokens" = "已跟踪 token";
"Subscriptions" = "订阅";
"By subscription" = "按订阅";
"By source" = "By source";
"%d of %d sources have spend" = "%d of %d sources have spend";
"No model-level history" = "暂无模型级历史";
"Daily estimated spend" = "每日估算支出";
"Hourly estimated spend" = "每小时估算支出";
Expand Down
2 changes: 2 additions & 0 deletions Sources/CodexBar/Resources/zh-Hant.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1379,6 +1379,8 @@
"Tracked tokens" = "已追蹤 token";
"Subscriptions" = "訂閱";
"By subscription" = "依訂閱";
"By source" = "By source";
"%d of %d sources have spend" = "%d of %d sources have spend";
"No model-level history" = "尚無模型層級歷史";
"Daily estimated spend" = "每日預估支出";
"Hourly estimated spend" = "每小時預估支出";
Expand Down
27 changes: 24 additions & 3 deletions Sources/CodexBar/SettingsStore+TokenCost.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,13 @@ extension SettingsStore {

Task { @MainActor [weak self] in
guard let self else { return }
let environment = ProcessInfo.processInfo.environment
let hasSources = await Task.detached(priority: .utility) {
Self.hasAnyTokenCostUsageSources()
let processContexts = await LocalAgentSessionScanner().piSessionProcessContexts(
environment: environment)
return Self.hasAnyTokenCostUsageSources(
env: environment,
processContexts: processContexts)
}.value
guard hasSources else { return }
guard UserDefaults.standard.object(forKey: "tokenCostUsageEnabled") == nil else { return }
Expand All @@ -36,9 +41,10 @@ extension SettingsStore {
env: [String: String] = ProcessInfo.processInfo.environment,
fileManager: FileManager = .default,
homeDirectory: URL? = nil,
workingDirectory: URL? = nil) -> Bool
workingDirectory: URL? = nil,
processContexts: [PiSessionProcessContext] = []) -> Bool
{
// Provider-specific by design: only Codex and Claude have local JSONL scanners that can auto-enable token cost.
// Provider-specific by design: Codex, Claude, and Pi-family stores can auto-enable token cost.
let home = homeDirectory ?? fileManager.homeDirectoryForCurrentUser

func hasAnyJsonl(in root: URL) -> Bool {
Expand Down Expand Up @@ -79,6 +85,21 @@ extension SettingsStore {
return true
}

var piEnvironment = env
if piEnvironment["HOME"]?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ?? true {
piEnvironment["HOME"] = home.path
}
let piBaseDirectory = workingDirectory ?? URL(
fileURLWithPath: fileManager.currentDirectoryPath,
isDirectory: true)
let piRoots = PiFamilySessionRootResolver.costSessionRootURLs(
environment: piEnvironment,
baseDirectory: piBaseDirectory,
processContexts: processContexts)
if piRoots.contains(where: hasAnyJsonl(in:)) {
return true
}

let claudeRoots: [URL] = {
if let configuredRoot = env[ClaudeConfigPaths.configDirectoryEnvironmentKey],
!configuredRoot.isEmpty
Expand Down
Loading
Loading