Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
554e85b
Add Grok local token cost tracking and main-menu usage chart.
chenow9 Jul 21, 2026
2c3484b
Align Grok cost charts with Codex spacing and teal palette.
chenow9 Jul 21, 2026
22307b1
Address review: multi-model Grok usage, public fixtures, strict widge…
chenow9 Jul 30, 2026
fbd15d5
Limit cost-chart teal palette to Grok only
chenow9 Jul 30, 2026
826f8e4
Fix CI lint and continuous cost-history dashboard test.
chenow9 Aug 2, 2026
6cc09ad
Bound Grok session-log scans with file and refresh budgets.
chenow9 Aug 2, 2026
5340209
Persist Grok session parse cache for budget-deferred catch-up.
chenow9 Aug 2, 2026
7bb60c4
Surface incomplete Grok history for oversized session logs.
chenow9 Aug 2, 2026
03bf0d2
Add final-head Grok Cost menu proof screenshot.
chenow9 Aug 3, 2026
e8b4034
Fix Grok ScanResult access level after rebase.
chenow9 Aug 3, 2026
e6c08ac
Document Grok cost in CLI and forward scanner cacheRoot.
chenow9 Aug 5, 2026
aefbeaa
Fix Grok turn meta parsing and session-log EOF flush.
chenow9 Aug 6, 2026
3718ed1
Merge upstream/main into feature/grok-local-token-cost.
chenow9 Aug 6, 2026
b4e6480
Adapt Grok cost dashboard to descriptor-driven menu wiring.
chenow9 Aug 6, 2026
c5b3363
Merge upstream/main into feature/grok-local-token-cost.
chenow9 Aug 6, 2026
7e1b1ab
Add exact-head Grok Cost menu proof screenshot.
chenow9 Aug 6, 2026
3b31d25
Fix Grok cost CLI project semantics and architecture guards.
chenow9 Aug 6, 2026
7bec70a
Merge upstream/main into feature/grok-local-token-cost.
chenow9 Aug 6, 2026
dbd9dea
Mark Grok session path scan as provider-specific.
chenow9 Aug 6, 2026
8ffe623
Add final-head Grok Cost menu and CLI project proofs.
chenow9 Aug 6, 2026
3843777
Redact unreleased model id from Grok Cost proof screenshots.
chenow9 Aug 6, 2026
ed17a2e
Keep Cost tracking opt-in for Grok-only upgrades.
chenow9 Aug 6, 2026
e7d6b0a
Add final-head Grok Cost CLI and opt-in default proofs.
chenow9 Aug 6, 2026
76bf2b8
Suppress Grok project self-sources in Cost CLI output.
chenow9 Aug 6, 2026
e69b4c0
Add final-head Grok CLI proof after self-source suppression.
chenow9 Aug 6, 2026
7fa84bc
Bound Grok turn cache load and save to prevent unbounded growth.
chenow9 Aug 6, 2026
2a36887
Redact Grok CLI proof projects and add cache-bounds evidence.
chenow9 Aug 6, 2026
51e2366
Warn incomplete Grok project totals and redact menu project labels.
chenow9 Aug 6, 2026
8ce15ae
Add final-head Grok CLI proof with incomplete-history note.
chenow9 Aug 6, 2026
4c43e70
Merge remote-tracking branch 'upstream/main' into feature/grok-local-…
chenow9 Aug 6, 2026
295eb7d
Merge upstream/main into feature/grok-local-token-cost.
chenow9 Aug 10, 2026
9e98448
Fix SwiftFormat lint after main merge.
chenow9 Aug 10, 2026
1fb1af6
Tighten Grok Cost cache retention policy.
chenow9 Aug 10, 2026
05a0f7a
Fence Grok cache saves against Cost-off races.
chenow9 Aug 10, 2026
5bb9acf
Add final-head Grok Cost retention and race-fence proof.
chenow9 Aug 10, 2026
602b298
Restamp Grok retention proof banner as final head.
chenow9 Aug 10, 2026
5ba725d
Refresh Grok retention proof for race-fence head.
chenow9 Aug 10, 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
529 changes: 370 additions & 159 deletions Sources/CodexBar/CostHistoryChartMenuView.swift

Large diffs are not rendered by default.

197 changes: 186 additions & 11 deletions Sources/CodexBar/InlineUsageDashboardContent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,17 @@ extension UsageMenuCardView.Model {
/// Provider branding color for the inline usage bars, matching the provider's switcher tab and
/// detailed cost-history chart.
static func inlineDashboardBarColor(for provider: UsageProvider) -> Color {
// Provider-specific by design: Grok cost/token history uses Codex teal.
if provider == .grok {
return self.codexStyleChartBarColor
}
let color = ProviderDescriptorRegistry.descriptor(for: provider).branding.color
return Color(red: color.red, green: color.green, blue: color.blue)
}

/// Codex brand teal — shared cost-chart palette (matches Credits/Codex cost bars).
static let codexStyleChartBarColor = Color(red: 73 / 255, green: 163 / 255, blue: 176 / 255)

private static func resolveInlineUsageDashboard(input: Input) -> InlineUsageDashboardModel? {
let menuCard = ProviderDescriptorRegistry.descriptor(for: input.provider).presentation.menuCard
if menuCard.usesProviderCostHistoryAsPrimaryDashboard,
Expand Down Expand Up @@ -185,14 +192,14 @@ extension UsageMenuCardView.Model {
} else {
L("%@ cost", historyDays == 1 ? L("Today") : String(format: L("Last %d days"), historyDays))
}
let points = snapshot.daily.suffix(historyDays).compactMap { entry -> InlineUsageDashboardModel.Point? in
guard let cost = entry.costUSD else { return nil }
return InlineUsageDashboardModel.Point(
id: entry.date,
label: Self.shortDayLabel(entry.date),
value: convertedValue(cost),
accessibilityValue: "\(entry.date): \(convertedString(cost))")
}
// Grok (and sparse-cost histories) plot daily tokens so subscription days without
// costUsdTicks still appear. Cost remains in KPIs/details.
let plotTokens = Self.shouldPlotTokensOnInlineCostChart(provider: provider, snapshot: snapshot)
let points = Self.inlineChartPoints(
snapshot: snapshot,
historyDays: historyDays,
plotTokens: plotTokens,
preferredCurrencyCode: preferredCurrencyCode)
let latest = CostUsageTokenSnapshot.latestEntry(in: snapshot.daily)
let usesLatestPrimary = tokenCost.primaryValue == .latestDaily
let primaryCostUSD = usesLatestPrimary ? latest?.costUSD : snapshot.sessionCostUSD
Expand All @@ -212,6 +219,13 @@ extension UsageMenuCardView.Model {
if let topModel = Self.topCostModel(from: snapshot.daily) {
details.append("\(L("Top model")): \(Self.shortModelName(topModel))")
}
// Provider-specific by design: Grok adds token composition and project notes on the cost card.
if provider == .grok {
details.append(contentsOf: Self.grokCostHistoryDetailLines(snapshot: snapshot))
}
if snapshot.historyIsIncomplete {
details.append(L("History incomplete: some large session logs were only partially scanned."))
}
let hintLines = Self.tokenUsageHintLines(provider: provider)
if tokenCost.hintPlacement == .beforeRequestHistory {
details.append(contentsOf: hintLines)
Expand All @@ -233,7 +247,9 @@ extension UsageMenuCardView.Model {
let accessibilityLabel = L(
"%@: %@",
providerName,
accessibilityCostLabel)
plotTokens
? (snapshot.historyLabel.map { "\($0) tokens" } ?? L("token usage"))
: accessibilityCostLabel)
var kpis = [
InlineUsageDashboardModel.KPI(
title: usesLatestPrimary ? L("Latest") : L("Today"),
Expand All @@ -245,6 +261,13 @@ extension UsageMenuCardView.Model {
.map(convertedString) ?? "—",
emphasis: false),
]
// Provider-specific by design: Grok leads with today's tokens to match token bars.
if provider == .grok {
let todayTokens = snapshot.sessionTokens.map(UsageFormatter.tokenCountString) ?? "—"
kpis.insert(
.init(title: L("Today tokens"), value: todayTokens, emphasis: true),
at: 0)
}
let tokenHistoryKPI = InlineUsageDashboardModel.KPI(
title: tokenHistoryTitle,
value: snapshot.last30DaysTokens.map(UsageFormatter.tokenCountString) ?? "—",
Expand All @@ -267,14 +290,166 @@ extension UsageMenuCardView.Model {
}
var model = InlineUsageDashboardModel(
accessibilityLabel: accessibilityLabel,
valueStyle: Self.costValueStyle(currencyCode: displayCurrencyCode),
valueStyle: plotTokens ? .tokens : Self.costValueStyle(currencyCode: displayCurrencyCode),
kpis: kpis,
points: points,
detailLines: details)
model.currencyCode = displayCurrencyCode
// Provider-specific by design: Grok cost chart bars use Codex teal.
if provider == .grok {
model.barColor = self.codexStyleChartBarColor
}
if !plotTokens {
model.currencyCode = displayCurrencyCode
}
return model
}

private static func grokCostHistoryDetailLines(snapshot: CostUsageTokenSnapshot) -> [String] {
var details: [String] = []
let input = snapshot.daily.compactMap(\.inputTokens).reduce(0, +)
let cache = snapshot.daily.compactMap(\.cacheReadTokens).reduce(0, +)
let output = snapshot.daily.compactMap(\.outputTokens).reduce(0, +)
if input + cache + output > 0 {
details.append(String(
format: L("Uncached %@ · Cache %@ · Output %@"),
UsageFormatter.tokenCountString(input),
UsageFormatter.tokenCountString(cache),
UsageFormatter.tokenCountString(output)))
}
let daysWithCost = snapshot.daily.count(where: { ($0.costUSD ?? 0) > 0 })
details.append(String(
format: L("Cost reported on %d/%d days"),
daysWithCost,
snapshot.daily.count))
if let topProject = snapshot.projects.first {
let tokens = topProject.totalTokens.map(UsageFormatter.tokenCountString) ?? "—"
details.append(String(format: L("Top project: %@ · %@"), topProject.name, tokens))
}
return details
}

/// Prefer token bars when cost history is incomplete (Grok subscription) or absent.
static func shouldPlotTokensOnInlineCostChart(
provider: UsageProvider,
snapshot: CostUsageTokenSnapshot) -> Bool
{
// Provider-specific by design: Grok subscription days often omit cost ticks.
if provider == .grok { return true }
let days = snapshot.daily
guard !days.isEmpty else { return false }
let withCost = days.count(where: { ($0.costUSD ?? 0) > 0 })
return withCost == 0
}

/// Continuous daily points for the main-menu mini chart (zero-fill gaps like Codex).
static func inlineChartPoints(
snapshot: CostUsageTokenSnapshot,
historyDays: Int,
plotTokens: Bool,
preferredCurrencyCode: String = "auto") -> [InlineUsageDashboardModel.Point]
{
let sorted = snapshot.daily.sorted { $0.date < $1.date }
guard let lastKey = sorted.last?.date else { return [] }
guard let end = Self.dateFromDayKey(lastKey) else {
return sorted.suffix(historyDays).compactMap { entry in
self.inlinePoint(
for: entry,
plotTokens: plotTokens,
providerCurrencyCode: snapshot.currencyCode,
preferredCurrencyCode: preferredCurrencyCode)
}
}
let calendar = Calendar.current
let start = calendar.date(byAdding: .day, value: -(historyDays - 1), to: calendar.startOfDay(for: end))
?? calendar.startOfDay(for: end)
var byDay: [String: CostUsageDailyReport.Entry] = [:]
for entry in sorted {
byDay[entry.date] = entry
}
var points: [InlineUsageDashboardModel.Point] = []
var cursor = calendar.startOfDay(for: start)
let endDay = calendar.startOfDay(for: end)
while cursor <= endDay {
let key = Self.dayKey(from: cursor)
if let entry = byDay[key],
let point = self.inlinePoint(
for: entry,
plotTokens: plotTokens,
providerCurrencyCode: snapshot.currencyCode,
preferredCurrencyCode: preferredCurrencyCode)
{
points.append(point)
} else {
points.append(InlineUsageDashboardModel.Point(
id: key,
label: Self.shortDayLabel(key),
value: 0,
accessibilityValue: "\(key): —"))
}
guard let next = calendar.date(byAdding: .day, value: 1, to: cursor) else { break }
cursor = next
}
return points
}

private static func inlinePoint(
for entry: CostUsageDailyReport.Entry,
plotTokens: Bool,
providerCurrencyCode: String,
preferredCurrencyCode: String) -> InlineUsageDashboardModel.Point?
{
if plotTokens {
let tokens = entry.totalTokens ?? 0
let costNote: String = {
guard let cost = entry.costUSD else { return "" }
let formatted = UsageFormatter.convertedCostString(
cost,
preferredCurrency: preferredCurrencyCode,
providerCurrency: providerCurrencyCode)
return " · \(formatted)"
}()
return InlineUsageDashboardModel.Point(
id: entry.date,
label: Self.shortDayLabel(entry.date),
value: Double(tokens),
accessibilityValue: "\(entry.date): \(UsageFormatter.tokenCountString(tokens)) tokens\(costNote)")
}
guard let cost = entry.costUSD else { return nil }
let converted = UsageFormatter.convertedCost(
cost,
preferredCurrency: preferredCurrencyCode,
providerCurrency: providerCurrencyCode)
let costString = UsageFormatter.convertedCostString(
cost,
preferredCurrency: preferredCurrencyCode,
providerCurrency: providerCurrencyCode)
return InlineUsageDashboardModel.Point(
id: entry.date,
label: Self.shortDayLabel(entry.date),
value: converted.value,
accessibilityValue: "\(entry.date): \(costString)")
}

private static func dateFromDayKey(_ key: String) -> Date? {
let parts = key.split(separator: "-")
guard parts.count == 3,
let year = Int(parts[0]),
let month = Int(parts[1]),
let day = Int(parts[2]) else { return nil }
var comps = DateComponents()
comps.calendar = Calendar.current
comps.year = year
comps.month = month
comps.day = day
comps.hour = 12
return comps.date
}

private static func dayKey(from date: Date) -> String {
let comps = Calendar.current.dateComponents([.year, .month, .day], from: date)
return String(format: "%04d-%02d-%02d", comps.year ?? 1970, comps.month ?? 1, comps.day ?? 1)
}

private static func costHistoryTrailingKPIs(
snapshot: CostUsageTokenSnapshot,
latest: CostUsageDailyReport.Entry?)
Expand Down
68 changes: 58 additions & 10 deletions Sources/CodexBar/MenuCardView+Costs.swift
Original file line number Diff line number Diff line change
Expand Up @@ -218,27 +218,75 @@ extension UsageMenuCardView.Model {
providerCurrency: snapshot.currencyCode)
return String(format: L("Cursor-metered: %@ (%@)"), amount, windowLabel.lowercased())
}
var comparisonLines: [String] = []
if comparisonPeriodsEnabled {
comparisonLines.append(contentsOf: snapshot.comparisonSummaries().map {
Self.costWindowLine(
summary: $0,
currencyCode: UsageFormatter.effectiveCurrencyCode(
preferred: preferredCurrencyCode,
providerCurrency: snapshot.currencyCode),
sourceCurrencyCode: snapshot.currencyCode)
})
}
// Provider-specific by design: Grok surfaces token composition on the Cost card.
if provider == .grok {
comparisonLines.append(contentsOf: Self.grokTokenDetailLines(snapshot: snapshot))
}
let err = (error?.isEmpty ?? true) ? nil : error
return TokenUsageSection(
isRefreshing: isRefreshing,
sessionLine: sessionLine,
monthLine: monthLine,
meteredLine: meteredLine,
comparisonLines: comparisonPeriodsEnabled
? snapshot.comparisonSummaries().map {
Self.costWindowLine(
summary: $0,
currencyCode: UsageFormatter.effectiveCurrencyCode(
preferred: preferredCurrencyCode,
providerCurrency: snapshot.currencyCode),
sourceCurrencyCode: snapshot.currencyCode)
}
: [],
comparisonLines: comparisonLines,
hintLine: Self.tokenUsageHint(provider: provider),
errorLine: err,
errorCopyText: (error?.isEmpty ?? true) ? nil : error)
}

/// Extra Cost-card lines for Grok local session stats.
static func grokTokenDetailLines(snapshot: CostUsageTokenSnapshot) -> [String] {
var lines: [String] = []
let input = snapshot.daily.compactMap(\.inputTokens).reduce(0, +)
let cache = snapshot.daily.compactMap(\.cacheReadTokens).reduce(0, +)
let output = snapshot.daily.compactMap(\.outputTokens).reduce(0, +)
if input + cache + output > 0 {
lines.append(String(
format: L("Uncached %@ · Cache %@ · Output %@"),
UsageFormatter.tokenCountString(input),
UsageFormatter.tokenCountString(cache),
UsageFormatter.tokenCountString(output)))
}
var modelTotals: [String: Int] = [:]
for entry in snapshot.daily {
for breakdown in entry.modelBreakdowns ?? [] {
modelTotals[breakdown.modelName, default: 0] += breakdown.totalTokens ?? 0
}
}
if !modelTotals.isEmpty {
let top = modelTotals.sorted { $0.value > $1.value }.prefix(3)
let parts = top.map { "\($0.key) \(UsageFormatter.tokenCountString($0.value))" }
lines.append(String(format: L("Models: %@"), parts.joined(separator: " · ")))
}
if !snapshot.projects.isEmpty {
let topProjects = snapshot.projects.prefix(3)
let parts = topProjects.map { project in
let tokens = project.totalTokens.map(UsageFormatter.tokenCountString) ?? "—"
return "\(project.name) \(tokens)"
}
lines.append(String(format: L("Top projects: %@"), parts.joined(separator: " · ")))
}
let daysWithCost = snapshot.daily.count(where: { ($0.costUSD ?? 0) > 0 })
if daysWithCost < snapshot.daily.count {
lines.append(String(
format: L("Cost reported on %d/%d days"),
daysWithCost,
snapshot.daily.count))
}
return lines
}

static func costWindowLine(
summary: CostUsageWindowSummary,
currencyCode: String,
Expand Down
9 changes: 5 additions & 4 deletions Sources/CodexBar/MenuHighlightStyle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ import SwiftUI

extension EnvironmentValues {
@Entry var menuItemHighlighted: Bool = false
/// Optional live-refresh monitor injected into menu card views so the provider card
/// subtitle can reflect the in-flight "Refreshing…" state in place while the NSMenu
/// stays open, without rebuilding the menu during AppKit tracking.
@Entry var menuCardRefreshMonitor: MenuCardRefreshMonitor?

// Optional live-refresh monitor injected into menu card views so the provider card
// subtitle can reflect the in-flight "Refreshing…" state in place while the NSMenu
// stays open, without rebuilding the menu during AppKit tracking.
@Entry var menuCardRefreshMonitor: MenuCardRefreshMonitor? = nil
}

enum MenuHighlightStyle {
Expand Down
7 changes: 7 additions & 0 deletions Sources/CodexBar/SettingsStore+Defaults.swift
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,13 @@ extension SettingsStore {
self.userDefaults.set(newValue, forKey: "tokenCostUsageEnabled")
if changed {
self.costUsageSettingsRevision &+= 1
// Grok local parse cache retains path/session metadata only while Cost is on.
// Invalidate write tokens + delete so in-flight scans cannot recreate the artifact.
if !newValue {
Task.detached(priority: .utility) {
GrokTurnUsageCacheIO.invalidateAndDelete()
}
}
}
self.noteBackgroundWorkSettingsChanged()
}
Expand Down
3 changes: 3 additions & 0 deletions Sources/CodexBar/SettingsStore+TokenCost.swift
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,9 @@ extension SettingsStore {
] + ClaudeDesktopProjectsLocator.roots(homeDirectory: ownerHome, fileManager: fileManager)
}()

// Grok session logs are not auto-enable sources: an absent `tokenCostUsageEnabled`
// preference must stay off on upgrade so existing Grok-only installs remain opt-in.
// Users enable Cost tracking explicitly; Grok scanning then runs via the descriptor.
return claudeRoots.contains(where: hasAnyJsonl(in:))
}
}
5 changes: 3 additions & 2 deletions Sources/CodexBar/StatusItemController+HostedSubmenus.swift
Original file line number Diff line number Diff line change
Expand Up @@ -419,8 +419,9 @@ extension StatusItemController {
currencyCode: tokenSnapshot.currencyCode,
historyDays: tokenSnapshot.historyDays,
windowLabel: tokenSnapshot.historyLabel,
projects: provider == .codex ? tokenSnapshot.projects : [],
sessions: provider == .codex ? tokenSnapshot.sessions : [],
// Codex + Grok both expose local project/session breakdowns from log scans.
projects: (provider == .codex || provider == .grok) ? tokenSnapshot.projects : [],
sessions: (provider == .codex || provider == .grok) ? tokenSnapshot.sessions : [],
width: width)
let hosting = MenuHostingView(rootView: chartView)
hosting.applyMeasuredHeight(
Expand Down
Loading