Skip to content
Open
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
- Claude: cache successful CLI version probes for 30 minutes while invalidating on executable changes, avoiding repeated PTY launches without retaining failed or stale wrapper results. Thanks @Yuxin-Qiao!
- Linux CLI: bootstrap the configured IANA timezone before Foundation startup on non-FHS systems, preventing SIGILL on NixOS (#2127). Thanks @xikhar!
- Ollama: release temporary dashboard network sessions after each fetch, preventing repeated refreshes from retaining delegates and URL-cache resources. Thanks @astuteprogrammer!
- DeepSeek: use a separate Platform web-session token for detailed usage, validate sessions across Chrome profiles,
offer a picker when multiple profiles are valid, fetch amount and cost concurrently, and preserve API-key balance
updates when detailed usage is unavailable.
- Linux CLI: prevent usage rendering from crashing in Foundation bundle discovery when formatting rate windows. Thanks @thanthi-del!
- Menus: keep overview provider-row clicks reliable during live menu rebuilds without stealing nested Copy or plan actions. Thanks @Yuxin-Qiao!
- Startup: load persisted plan-utilization history away from the main thread so mature histories no longer delay app launch. Thanks @Yuxin-Qiao!
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
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
@@ -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
28 changes: 28 additions & 0 deletions Sources/CodexBar/Providers/DeepSeek/DeepSeekSettingsStore.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import CodexBarCore
import Foundation

extension SettingsStore {
func deepseekProfileID(apiKey: String?) -> String {
_ = self.configRevision
_ = self.providerDetailSettingsRevision
guard let config = self.config.providerConfig(for: .deepseek),
let profileID = config.sanitizedDeepSeekProfileID
else { return "" }
let accountID = self.selectedTokenAccount(for: .deepseek)?.id
let expectedScope = DeepSeekSettingsReader.profileScope(selectedTokenAccountID: accountID, apiKey: apiKey)
guard let expectedScope, config.sanitizedDeepSeekProfileScope == expectedScope else { return "" }
return profileID
}

func setDeepSeekProfileID(_ newValue: String, apiKey: String?) {
let profileID = self.normalizedConfigValue(newValue)
let profileScope = DeepSeekSettingsReader.profileScope(
selectedTokenAccountID: self.selectedTokenAccount(for: .deepseek)?.id,
apiKey: apiKey)
guard profileID == nil || profileScope != nil else { return }
self.updateProviderDetailConfig(provider: .deepseek) { entry in
entry.deepseekProfileID = profileID
entry.deepseekProfileScope = profileID == nil ? nil : profileScope
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,18 @@ enum ProviderTokenAccountSelection {
if let override, override.provider == provider { return override.account }
return settings.selectedTokenAccount(for: provider)
}

@MainActor
static func shouldIncludeOptionalUsage(
provider: UsageProvider,
settings: SettingsStore,
override: TokenAccountOverride?) -> Bool
{
guard settings.showOptionalCreditsAndExtraUsage else { return false }
guard provider == .deepseek,
let override,
override.provider == provider
else { return true }
return settings.selectedTokenAccount(for: provider)?.id == override.account.id
}
}
7 changes: 7 additions & 0 deletions Sources/CodexBar/Resources/ar.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -898,6 +898,13 @@
"MiniMax 30 day token usage trend" = "MiniMax اتجاه استخدام الرموز خلال 30 يوما";
"Today cash" = "اليوم نقدا";
"DeepSeek 30 day token usage trend" = "DeepSeek اتجاه استخدام الرموز خلال 30 يوما";
"Detailed usage unavailable." = "الاستخدام التفصيلي غير متاح.";
"Sign in to DeepSeek Platform in Chrome for detailed usage." = "سجّل الدخول إلى منصة DeepSeek في Chrome لعرض الاستخدام التفصيلي.";
"Select a DeepSeek Chrome profile in Settings." = "حدد ملف تعريف Chrome لـ DeepSeek في الإعدادات.";
"DeepSeek this month token usage trend" = "اتجاه استخدام رموز DeepSeek لهذا الشهر";
"Chrome profile" = "ملف تعريف Chrome";
"Choose which signed-in DeepSeek Platform session supplies detailed usage." = "اختر جلسة DeepSeek Platform المسجّل دخولها التي توفّر تفاصيل الاستخدام.";
"Select profile…" = "اختر ملفًا شخصيًا…";
"cache-hit input" = "إدخال الضربات المؤقتة";
"cache-miss input" = "إدخال ذاكرة تخزين مؤقت (CACHE-miss)";
"output" = "الإنتاج";
Expand Down
7 changes: 7 additions & 0 deletions Sources/CodexBar/Resources/ca.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -761,6 +761,13 @@
"MiniMax 30 day token usage trend" = "Tendència d'ús de tokens de 30 dies de MiniMax";
"Today cash" = "Efectiu d'avui";
"DeepSeek 30 day token usage trend" = "Tendència d'ús de tokens de 30 dies de DeepSeek";
"Detailed usage unavailable." = "L'ús detallat no està disponible.";
"Sign in to DeepSeek Platform in Chrome for detailed usage." = "Inicia la sessió a DeepSeek Platform al Chrome per veure l'ús detallat.";
"Select a DeepSeek Chrome profile in Settings." = "Selecciona un perfil de Chrome de DeepSeek a Configuració.";
"DeepSeek this month token usage trend" = "Tendència d'ús de tokens de DeepSeek aquest mes";
"Chrome profile" = "Perfil de Chrome";
"Choose which signed-in DeepSeek Platform session supplies detailed usage." = "Tria quina sessió iniciada de DeepSeek Platform proporciona l'ús detallat.";
"Select profile…" = "Selecciona un perfil…";
"cache-hit input" = "entrada amb encert de memòria cau";
"cache-miss input" = "entrada sense encert de memòria cau";
"output" = "sortida";
Expand Down
7 changes: 7 additions & 0 deletions Sources/CodexBar/Resources/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -899,6 +899,13 @@
"MiniMax 30 day token usage trend" = "MiniMax 30 day token usage trend";
"Today cash" = "Today cash";
"DeepSeek 30 day token usage trend" = "DeepSeek 30 day token usage trend";
"DeepSeek this month token usage trend" = "DeepSeek this month token usage trend";
"Chrome profile" = "Chrome profile";
"Choose which signed-in DeepSeek Platform session supplies detailed usage." = "Choose which signed-in DeepSeek Platform session supplies detailed usage.";
"Detailed usage unavailable." = "Detailed usage unavailable.";
"Sign in to DeepSeek Platform in Chrome for detailed usage." = "Sign in to DeepSeek Platform in Chrome for detailed usage.";
"Select a DeepSeek Chrome profile in Settings." = "Select a DeepSeek Chrome profile in Settings.";
"Select profile…" = "Select profile…";
"cache-hit input" = "cache-hit input";
"cache-miss input" = "cache-miss input";
"output" = "output";
Expand Down
7 changes: 7 additions & 0 deletions Sources/CodexBar/Resources/fa.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -898,6 +898,13 @@
"MiniMax 30 day token usage trend" = "MiniMax روند استفاده ۳۰ روزه از توکن";
"Today cash" = "امروزه پول نقد";
"DeepSeek 30 day token usage trend" = "DeepSeek روند استفاده ۳۰ روزه از توکن";
"Detailed usage unavailable." = "جزئیات استفاده در دسترس نیست.";
"Sign in to DeepSeek Platform in Chrome for detailed usage." = "برای مشاهده جزئیات استفاده، در Chrome وارد DeepSeek Platform شوید.";
"Select a DeepSeek Chrome profile in Settings." = "یک نمایه Chrome دیپ‌سیک را در تنظیمات انتخاب کنید.";
"DeepSeek this month token usage trend" = "روند استفاده از توکن DeepSeek در این ماه";
"Chrome profile" = "نمایه Chrome";
"Choose which signed-in DeepSeek Platform session supplies detailed usage." = "انتخاب کنید کدام نشست واردشدهٔ DeepSeek Platform جزئیات استفاده را ارائه دهد.";
"Select profile…" = "انتخاب نمایه…";
"cache-hit input" = "ورودی کش و ضربه";
"cache-miss input" = "ورودی کش-خطا";
"output" = "خروجی";
Expand Down
7 changes: 7 additions & 0 deletions Sources/CodexBar/Resources/gl.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -755,6 +755,13 @@
"MiniMax 30 day token usage trend" = "Tendencia de uso de tokens de MiniMax en 30 días";
"Today cash" = "Gasto de hoxe";
"DeepSeek 30 day token usage trend" = "Tendencia de uso de tokens de DeepSeek en 30 días";
"DeepSeek this month token usage trend" = "Tendencia de uso de tokens de DeepSeek este mes";
"Chrome profile" = "Perfil de Chrome";
"Choose which signed-in DeepSeek Platform session supplies detailed usage." = "Escolle que sesión iniciada de DeepSeek Platform fornece o uso detallado.";
"Detailed usage unavailable." = "O uso detallado non está dispoñible.";
"Sign in to DeepSeek Platform in Chrome for detailed usage." = "Inicia sesión en DeepSeek Platform en Chrome para ver o uso detallado.";
"Select a DeepSeek Chrome profile in Settings." = "Selecciona un perfil de Chrome de DeepSeek en Axustes.";
"Select profile…" = "Seleccionar perfil…";
"cache-hit input" = "entrada atopada na caché";
"cache-miss input" = "entrada non atopada na caché";
"output" = "saída";
Expand Down
7 changes: 7 additions & 0 deletions Sources/CodexBar/Resources/id.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -900,6 +900,13 @@
"MiniMax 30 day token usage trend" = "Tren penggunaan token 30 hari MiniMax";
"Today cash" = "Kas hari ini";
"DeepSeek 30 day token usage trend" = "Tren penggunaan token 30 hari DeepSeek";
"DeepSeek this month token usage trend" = "Tren penggunaan token DeepSeek bulan ini";
"Chrome profile" = "Profil Chrome";
"Choose which signed-in DeepSeek Platform session supplies detailed usage." = "Pilih sesi DeepSeek Platform yang telah masuk untuk menyediakan rincian penggunaan.";
"Detailed usage unavailable." = "Rincian penggunaan tidak tersedia.";
"Sign in to DeepSeek Platform in Chrome for detailed usage." = "Masuk ke DeepSeek Platform di Chrome untuk melihat rincian penggunaan.";
"Select a DeepSeek Chrome profile in Settings." = "Pilih profil Chrome DeepSeek di Pengaturan.";
"Select profile…" = "Pilih profil…";
"cache-hit input" = "input cache-hit";
"cache-miss input" = "input cache-miss";
"output" = "output";
Expand Down
7 changes: 7 additions & 0 deletions Sources/CodexBar/Resources/it.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -900,6 +900,13 @@
"MiniMax 30 day token usage trend" = "Trend utilizzo token 30 giorni MiniMax";
"Today cash" = "Spesa di oggi";
"DeepSeek 30 day token usage trend" = "Trend utilizzo token 30 giorni DeepSeek";
"DeepSeek this month token usage trend" = "Trend utilizzo token DeepSeek di questo mese";
"Chrome profile" = "Profilo Chrome";
"Choose which signed-in DeepSeek Platform session supplies detailed usage." = "Scegli quale sessione di DeepSeek Platform con accesso effettuato fornisce l'utilizzo dettagliato.";
"Detailed usage unavailable." = "Utilizzo dettagliato non disponibile.";
"Sign in to DeepSeek Platform in Chrome for detailed usage." = "Accedi a DeepSeek Platform in Chrome per l'utilizzo dettagliato.";
"Select a DeepSeek Chrome profile in Settings." = "Seleziona un profilo Chrome di DeepSeek nelle Impostazioni.";
"Select profile…" = "Seleziona profilo…";
"cache-hit input" = "input cache-hit";
"cache-miss input" = "input cache-miss";
"output" = "uscita";
Expand Down
Loading
Loading