Skip to content
Closed
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

- Codex: add an opt-in "Include OpenCode local history (OpenAI)" source that merges machine-local OpenCode usage for `openai`-provider assistant records into the ambient Codex menu/CLI history, with read-only SQLite access, stable-ID deduplication across OpenCode's v1/v2 tables, recorded-cost-as-is semantics, and permanent exclusion from managed accounts, scoped `CODEX_HOME` scans (argument or environment), and the Spend Dashboard (refs #3273). Merged rows carry a visible `(OpenCode local)` model label, source-recorded cost provenance (`sourceRecorded`/`mixed`, never a pure list-price claim), and join the unattributed project bucket in grouped totals while session breakdowns stay native. Database selection mirrors upstream: `OPENCODE_DB` selects exactly one database (`:memory:` reads as no history, never a substitute), otherwise the release database under the XDG data root. Day buckets follow the reporting calendar with a start-of-day lower bound.
- Providers: add Claude Platform for Console API credit balances, expiring subscription and purchased grants, and optional monthly spend, using its signed-in browser session (#4510). Thanks @mjaverto!
- About: show bundled open-source acknowledgements and bundle SwiftPM and vendored dependency notices with the app, widget, CLI, and Linux desktop (#4483). Thanks @elijahfriedman!

Expand Down
24 changes: 21 additions & 3 deletions Sources/CodexBar/CostHistoryChartMenuView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ struct CostHistoryChartMenuView: View {
}

private let provider: UsageProvider
private let costProvenance: CostProvenance
private let daily: [DailyEntry]
private let totalCostUSD: Double?
private let currencyCode: String
Expand Down Expand Up @@ -96,9 +97,11 @@ struct CostHistoryChartMenuView: View {
sessions: [CostUsageSessionBreakdown] = [],
hidePersonalInfo: Bool,
width: CGFloat,
costProvenance: CostProvenance = .listPriceEstimate,
onMetricChanged: (@MainActor (ChartMetric) -> Void)? = nil)
{
self.provider = provider
self.costProvenance = costProvenance
self.daily = daily
self.totalCostUSD = totalCostUSD
self.currencyCode = currencyCode
Expand Down Expand Up @@ -338,7 +341,7 @@ struct CostHistoryChartMenuView: View {
if self.totalCostUSD != nil || incompleteCount > 0 {
VStack(alignment: .leading, spacing: 2) {
Text(String(
format: incompleteCount > 0 ? L("Est. subtotal (%@): %@") : L("Est. total (%@): %@"),
format: self.costTotalLabelKey(incompleteCount: incompleteCount),
self.windowLabel,
self.totalCostUSD.map(self.costString) ?? "—")
+ UsageFormatter.incompleteUsageSuffix(incompleteCount))
Expand All @@ -348,7 +351,7 @@ struct CostHistoryChartMenuView: View {
.truncationMode(.head)
.frame(height: Self.detailPrimaryLineHeight, alignment: .leading)
if let disclaimer = UsageFormatter.incompleteUsageNote(incompleteCount)
?? Self.estimateDisclaimer(provider: self.provider)
?? Self.estimateDisclaimer(provider: self.provider, provenance: self.costProvenance)
{
Text(disclaimer)
.help(disclaimer)
Expand Down Expand Up @@ -410,7 +413,22 @@ struct CostHistoryChartMenuView: View {
}
}

static func estimateDisclaimer(provider: UsageProvider) -> String? {
// Provider-specific by design: only Codex history can carry source-recorded OpenCode
// costs, so only its chart total label and disclaimer follow provenance like the
// menu-card hint does.
private func costTotalLabelKey(incompleteCount: Int) -> String {
guard self.provider == .codex, self.costProvenance == .sourceRecorded else {
return incompleteCount > 0 ? "Est. subtotal (%@): %@" : "Est. total (%@): %@"
}
return incompleteCount > 0 ? "Recorded subtotal (%@): %@" : "Recorded total (%@): %@"
}

static func estimateDisclaimer(provider: UsageProvider, provenance: CostProvenance = .listPriceEstimate) -> String? {
if provider == .codex, provenance == .sourceRecorded || provenance == .mixed {
return provenance == .mixed
? L("Estimated from token usage plus source-recorded OpenCode cost · not a subscription bill")
: L("Source-recorded cost from OpenCode local history · not a subscription bill")
}
guard let hint = ProviderDescriptorRegistry.descriptor(for: provider).tokenCost.chartEstimateDisclaimer else {
return nil
}
Expand Down
4 changes: 2 additions & 2 deletions Sources/CodexBar/InlineUsageDashboardContent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@ extension UsageMenuCardView.Model {
details.append("\(L("Top model")): \(Self.shortModelName(topModel))")
}
let tokenCost = ProviderDescriptorRegistry.descriptor(for: input.provider).tokenCost
let hintLines = Self.tokenUsageHintLines(provider: input.provider)
let hintLines = Self.tokenUsageHintLines(provider: input.provider, provenance: snapshot.costProvenance)
if tokenCost.hintPlacement == .beforeRequestHistory {
details.append(contentsOf: hintLines)
}
Expand Down Expand Up @@ -499,7 +499,7 @@ extension UsageMenuCardView.Model {
value: L("%@ tokens", snapshot.last30DaysTokens.map(UsageFormatter.tokenCountString) ?? "—"),
emphasis: false))
}
var details = Self.tokenUsageHintLines(provider: provider)
var details = Self.tokenUsageHintLines(provider: provider, provenance: snapshot.costProvenance)
if details.isEmpty { details.append(L("Local token history · dollar costs unavailable")) }
if let coverage = Self.tokenHistoryCoverageHint(snapshot) { details.append(coverage) }
if comparisonPeriodsEnabled {
Expand Down
23 changes: 18 additions & 5 deletions Sources/CodexBar/MenuCardView+Costs.swift
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ extension UsageMenuCardView.Model {
let todayIncompleteCount = snapshot.summary(forLastDays: 1, calendar: calendar).incompleteRequestCount
let err = (error?.isEmpty ?? true) ? nil : error
let hints = [
Self.tokenUsageHint(provider: provider),
Self.tokenUsageHint(provider: provider, provenance: snapshot.costProvenance),
UsageFormatter.incompleteUsageNote(incompleteCount),
tokensOnly ? Self.tokenHistoryCoverageHint(snapshot) : nil,
]
Expand Down Expand Up @@ -334,8 +334,11 @@ extension UsageMenuCardView.Model {
UsageFormatter.tokenCountString(totalTokens)) + suffix
}

static func tokenUsageHint(provider: UsageProvider) -> String? {
let lines = Self.tokenUsageHintLines(provider: provider)
static func tokenUsageHint(
provider: UsageProvider,
provenance: CostProvenance = .listPriceEstimate) -> String?
{
let lines = Self.tokenUsageHintLines(provider: provider, provenance: provenance)
return lines.isEmpty ? nil : lines.joined(separator: "\n")
}

Expand All @@ -356,8 +359,18 @@ extension UsageMenuCardView.Model {
: L("Local token history is unavailable or incomplete.")
}

static func tokenUsageHintLines(provider: UsageProvider) -> [String] {
ProviderDescriptorRegistry.descriptor(for: provider).tokenCost.menuHintLines.map { hint in
static func tokenUsageHintLines(
provider: UsageProvider,
provenance: CostProvenance = .listPriceEstimate) -> [String]
{
// Provider-specific by design: only Codex history can mix source-recorded OpenCode
// costs into an otherwise list-price estimate, so only that hint follows provenance.
if provider == .codex, provenance == .sourceRecorded || provenance == .mixed {
return [provenance == .mixed
? L("Estimated from token usage plus source-recorded OpenCode cost · not a subscription bill")
: L("Source-recorded cost from OpenCode local history · not a subscription bill")]
}
return ProviderDescriptorRegistry.descriptor(for: provider).tokenCost.menuHintLines.map { hint in
switch hint {
case let .localized(key): L(key)
case .estimate: UsageFormatter.costEstimateHint(provider: provider)
Expand Down
9 changes: 6 additions & 3 deletions Sources/CodexBar/PreferencesProviderDetailView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -160,11 +160,14 @@ struct ProviderDetailView<SupplementaryContent: View>: View {
Section {
ProviderTokenUsageInlineView(provider: self.provider, tokenUsage: tokenUsage)
} header: {
Text(L(self.store.settings.codexLocalSessionCostLedgerEnabled ? "This Mac" : "Local usage"))
Text(L(self.store.settings.codexLocalSessionCostLedgerEnabled
|| self.store.settings.codexIncludeOpenCodeLocalHistory
? "This Mac" : "Local usage"))
} footer: {
SettingsSectionFooter(L(self.store.settings.codexLocalSessionCostLedgerEnabled
? "Local usage is shared across accounts on this Mac."
: "Local usage comes from the current Codex profile."))
|| self.store.settings.codexIncludeOpenCodeLocalHistory
? "Local usage is shared across accounts on this Mac."
: "Local usage comes from the current Codex profile."))
}
}
if let diagnostic = self.openAIWebDiagnostic {
Expand Down
1 change: 1 addition & 0 deletions Sources/CodexBar/PreferencesSpendDashboardPane.swift
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ func spendDashboardProvenanceText(_ provenance: CostProvenance) -> String {
switch provenance {
case .listPriceEstimate: L("List-price equivalent")
case .vendorMetered: L("Plan metered")
case .sourceRecorded: L("Source recorded")
case .mixed: L("Metered and list-price")
case .unknown: L("Spend unavailable")
}
Expand Down
17 changes: 17 additions & 0 deletions Sources/CodexBar/Providers/Codex/CodexProviderImplementation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,23 @@ struct CodexProviderImplementation: ProviderImplementation {
onChange: nil,
onAppDidBecomeActive: nil,
onAppearWhenEnabled: nil),
ProviderSettingsToggleDescriptor(
id: "codex-opencode-local-history",
title: "Include OpenCode local history (OpenAI)",
subtitle: [
"Optional, off by default.",
"Merges machine-local OpenCode usage for OpenAI-provider assistant records from ~/.local/share/opencode/opencode.db into this Mac's ambient Codex history.",
"Reads usage metadata only (read-only SQLite: model, tokens, recorded cost); prompts, responses and auth state are never read.",
"OpenCode records $0 cost for subscription usage; recorded dollars are shown as-is and never treated as billing.",
"Rows are attributed to the model only, never to an account, and are excluded from managed accounts, scoped CODEX_HOME scans and the Spend Dashboard.",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Label OpenCode history as machine-wide in provider settings

When this opt-in is the only local toggle and a Codex account overview is present, the selected cost scope is codex:ambient, yet PreferencesProviderDetailView chooses its heading and footer only from codexLocalSessionCostLedgerEnabled; it therefore labels these machine-wide rows as “Local usage comes from the current Codex profile.” That contradicts this never-account-attributed contract and can make recorded costs look owned by the selected managed/profile account, so the label should use the effective ambient scope.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 29eeef6 — PreferencesProviderDetailView now picks the "This Mac" heading and "shared across accounts" footer from codexLocalSessionCostLedgerEnabled || codexIncludeOpenCodeLocalHistory, so machine-wide rows are never labeled as the selected profile's while the opt-in selects the ambient scope.

].joined(separator: " "),
binding: context.binding(\.codexIncludeOpenCodeLocalHistory),
statusText: nil,
actions: [],
isVisible: nil,
onChange: nil,
onAppDidBecomeActive: nil,
onAppearWhenEnabled: nil),
ProviderSettingsToggleDescriptor(
id: "codex-historical-tracking",
title: "Historical tracking",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,20 @@ struct CodexAccountScopedRefreshGuard: Equatable {
@MainActor
extension UsageStore {
func accountScopedTokenSnapshot(for provider: UsageProvider) -> CostUsageTokenSnapshot? {
guard provider == .codex, !self.settings.codexLocalSessionCostLedgerEnabled else {
guard provider == .codex else {
return self.tokenSnapshotPublications[provider.instanceID]?.snapshot
}
if self.settings.codexIncludeOpenCodeLocalHistory {
// Ambient history is account-agnostic: an ambient publication stays valid across
// account switches (config-revision churn) as long as the ambient scope itself is
// still selected — but a pre-toggle scoped publication must not leak account rows
// into the machine-local lane, so the scope signature has to match.
guard let publication = self.tokenSnapshotPublications[provider.instanceID],
publication.scopeSignature == self.tokenSnapshotScopeSignature(for: provider)
else { return nil }
return publication.snapshot
}
guard !self.settings.codexLocalSessionCostLedgerEnabled else {
return self.tokenSnapshotPublications[provider.instanceID]?.snapshot
}
return self.tokenSnapshotForCurrentProviderConfig(for: provider)?.snapshot
Expand Down
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 @@ -1636,6 +1636,8 @@
"Usage snapshot unavailable" = "ملخص الاستخدام غير متاح";
"CodexBar couldn't prepare a shareable usage snapshot. Refresh Usage & Spend and try again." = "تعذر على CodexBar إعداد ملخص استخدام قابل للمشاركة. حدّث الاستخدام والإنفاق ثم حاول مرة أخرى.";
"Incomplete" = "غير مكتمل";
"Recorded total (%@): %@" = "إجمالي مسجل (%@): %@";
"Recorded subtotal (%@): %@" = "إجمالي فرعي مسجل (%@): %@";
"Est. subtotal (%@): %@" = "الإجمالي الفرعي التقديري (%@): %@";
"Excluded requests with missing final usage: %d" = "الطلبات المستبعدة لعدم توفر بيانات الاستخدام النهائية: %d";

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 @@ -1636,6 +1636,8 @@
"Usage snapshot unavailable" = "Instantània d'ús no disponible";
"CodexBar couldn't prepare a shareable usage snapshot. Refresh Usage & Spend and try again." = "CodexBar no ha pogut preparar una instantània d'ús per compartir. Actualitza Ús i despesa i torna-ho a provar.";
"Incomplete" = "Incomplet";
"Recorded total (%@): %@" = "Total registrat (%@): %@";
"Recorded subtotal (%@): %@" = "Subtotal registrat (%@): %@";
"Est. subtotal (%@): %@" = "Subtotal estimat (%@): %@";
"Excluded requests with missing final usage: %d" = "Sol·licituds excloses sense dades d’ús finals: %d";

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 @@ -1634,6 +1634,8 @@
"Usage snapshot unavailable" = "Nutzungsübersicht nicht verfügbar";
"CodexBar couldn't prepare a shareable usage snapshot. Refresh Usage & Spend and try again." = "CodexBar konnte keine teilbare Nutzungsübersicht erstellen. Aktualisieren Sie „Nutzung & Ausgaben“ und versuchen Sie es erneut.";
"Incomplete" = "Unvollständig";
"Recorded total (%@): %@" = "Erfasster Gesamtbetrag (%@): %@";
"Recorded subtotal (%@): %@" = "Erfasster Teilbetrag (%@): %@";
"Est. subtotal (%@): %@" = "Geschätzte Zwischensumme (%@): %@";
"Excluded requests with missing final usage: %d" = "Ausgeschlossene Anfragen ohne abschließende Nutzungsdaten: %d";

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 @@ -1635,6 +1635,8 @@
"CodexBar couldn't prepare a shareable usage snapshot. Refresh Usage & Spend and try again." = "CodexBar couldn't prepare a shareable usage snapshot. Refresh Usage & Spend and try again.";
"Incomplete" = "Incomplete";

"Recorded total (%@): %@" = "Recorded total (%@): %@";
"Recorded subtotal (%@): %@" = "Recorded subtotal (%@): %@";
"Est. subtotal (%@): %@" = "Est. subtotal (%@): %@";

"Excluded requests with missing final usage: %d" = "Excluded requests with missing final usage: %d";
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 @@ -1630,6 +1630,8 @@
"Usage snapshot unavailable" = "Instantánea de uso no disponible";
"CodexBar couldn't prepare a shareable usage snapshot. Refresh Usage & Spend and try again." = "CodexBar no pudo preparar una instantánea de uso para compartir. Actualiza Uso y gasto y vuelve a intentarlo.";
"Incomplete" = "Incompleto";
"Recorded total (%@): %@" = "Total registrado (%@): %@";
"Recorded subtotal (%@): %@" = "Subtotal registrado (%@): %@";
"Est. subtotal (%@): %@" = "Subtotal estimado (%@): %@";
"Excluded requests with missing final usage: %d" = "Solicitudes excluidas sin datos de uso finales: %d";

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 @@ -1636,6 +1636,8 @@
"Usage snapshot unavailable" = "نمای کلی استفاده در دسترس نیست";
"CodexBar couldn't prepare a shareable usage snapshot. Refresh Usage & Spend and try again." = "CodexBar نتوانست نمای کلی استفاده قابل اشتراک‌گذاری را آماده کند. استفاده و هزینه را تازه‌سازی کنید و دوباره امتحان کنید.";
"Incomplete" = "ناقص";
"Recorded total (%@): %@" = "مجموع ثبت‌شده (%@): %@";
"Recorded subtotal (%@): %@" = "جمع جزئی ثبت‌شده (%@): %@";
"Est. subtotal (%@): %@" = "جمع جزئی تخمینی (%@): %@";
"Excluded requests with missing final usage: %d" = "درخواست‌های حذف‌شده به دلیل نبود داده‌های مصرف نهایی: %d";

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 @@ -1631,6 +1631,8 @@
"Usage snapshot unavailable" = "Aperçu de l’utilisation indisponible";
"CodexBar couldn't prepare a shareable usage snapshot. Refresh Usage & Spend and try again." = "CodexBar n’a pas pu préparer un aperçu de l’utilisation à partager. Actualisez Utilisation et dépenses, puis réessayez.";
"Incomplete" = "Incomplet";
"Recorded total (%@): %@" = "Total enregistré (%@) : %@";
"Recorded subtotal (%@): %@" = "Sous-total enregistré (%@) : %@";
"Est. subtotal (%@): %@" = "Sous-total estimé (%@) : %@";
"Excluded requests with missing final usage: %d" = "Requêtes exclues sans données d’utilisation finales : %d";

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 @@ -1631,6 +1631,8 @@
"Usage snapshot unavailable" = "Resumo do uso non dispoñible";
"CodexBar couldn't prepare a shareable usage snapshot. Refresh Usage & Spend and try again." = "CodexBar non puido preparar un resumo do uso para compartir. Actualiza Uso e gasto e téntao de novo.";
"Incomplete" = "Incompleto";
"Recorded total (%@): %@" = "Total rexistrado (%@): %@";
"Recorded subtotal (%@): %@" = "Subtotal rexistrado (%@): %@";
"Est. subtotal (%@): %@" = "Subtotal estimado (%@): %@";
"Excluded requests with missing final usage: %d" = "Solicitudes excluídas sen datos de uso finais: %d";

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 @@ -1635,6 +1635,8 @@
"Usage snapshot unavailable" = "Ringkasan penggunaan tidak tersedia";
"CodexBar couldn't prepare a shareable usage snapshot. Refresh Usage & Spend and try again." = "CodexBar tidak dapat menyiapkan ringkasan penggunaan untuk dibagikan. Segarkan Penggunaan & pengeluaran, lalu coba lagi.";
"Incomplete" = "Tidak lengkap";
"Recorded total (%@): %@" = "Total tercatat (%@): %@";
"Recorded subtotal (%@): %@" = "Subtotal tercatat (%@): %@";
"Est. subtotal (%@): %@" = "Subtotal perkiraan (%@): %@";
"Excluded requests with missing final usage: %d" = "Permintaan yang dikecualikan karena data penggunaan akhir tidak tersedia: %d";

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 @@ -1636,6 +1636,8 @@
"Usage snapshot unavailable" = "Riepilogo utilizzo non disponibile";
"CodexBar couldn't prepare a shareable usage snapshot. Refresh Usage & Spend and try again." = "CodexBar non ha potuto preparare un riepilogo dell’utilizzo da condividere. Aggiorna Utilizzo e spesa e riprova.";
"Incomplete" = "Incompleto";
"Recorded total (%@): %@" = "Totale registrato (%@): %@";
"Recorded subtotal (%@): %@" = "Subtotale registrato (%@): %@";
"Est. subtotal (%@): %@" = "Subtotale stimato (%@): %@";
"Excluded requests with missing final usage: %d" = "Richieste escluse senza dati di utilizzo finali: %d";

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 @@ -1632,6 +1632,8 @@
"Usage snapshot unavailable" = "使用状況の概要を利用できません";
"CodexBar couldn't prepare a shareable usage snapshot. Refresh Usage & Spend and try again." = "CodexBar で共有用の使用状況の概要を準備できませんでした。「使用状況と支出」を更新して、もう一度お試しください。";
"Incomplete" = "不完全";
"Recorded total (%@): %@" = "記録された合計 (%@): %@";
"Recorded subtotal (%@): %@" = "記録された小計 (%@): %@";
"Est. subtotal (%@): %@" = "推定小計 (%@): %@";
"Excluded requests with missing final usage: %d" = "最終的な使用量が未記録のため除外されたリクエスト: %d";

Expand Down
Loading