Skip to content
Merged
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)

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 Do not preserve web-profile balances across profile changes

When DeepSeek is forced to the web source while an API key or token account is still configured, apiKey != nil makes the transition preserve the old primary balance even though web-mode balances come from the selected Platform/Chrome profile, not from the API key. If the user switches from one Chrome profile to another and the refresh is slow or fails, the UI can keep showing the previous profile's balance under the new profile selection; use the active source/last source instead of API-key presence before preserving the balance.

Useful? React with 👍 / 👎.

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.effectiveSelectedTokenAccount(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 @@ -902,6 +902,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 @@ -765,6 +765,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 @@ -903,6 +903,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 @@ -902,6 +902,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 @@ -759,6 +759,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 @@ -904,6 +904,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 @@ -904,6 +904,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
7 changes: 7 additions & 0 deletions Sources/CodexBar/Resources/pl.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -904,6 +904,13 @@
"MiniMax 30 day token usage trend" = "Trend użycia tokenów MiniMax z 30 dni";
"Today cash" = "Dzisiejsza gotówka";
"DeepSeek 30 day token usage trend" = "Trend użycia tokenów DeepSeek z 30 dni";
"DeepSeek this month token usage trend" = "Trend użycia tokenów DeepSeek w tym miesiącu";
"Chrome profile" = "Profil Chrome";
"Choose which signed-in DeepSeek Platform session supplies detailed usage." = "Wybierz zalogowaną sesję DeepSeek Platform, która ma dostarczać szczegółowe dane użycia.";
"Detailed usage unavailable." = "Szczegółowe dane użycia są niedostępne.";
"Sign in to DeepSeek Platform in Chrome for detailed usage." = "Zaloguj się do DeepSeek Platform w Chrome, aby uzyskać szczegółowe dane użycia.";
"Select a DeepSeek Chrome profile in Settings." = "Wybierz profil Chrome DeepSeek w Ustawieniach.";
"Select profile…" = "Wybierz profil…";
"cache-hit input" = "wejście trafienia pamięci podręcznej";
"cache-miss input" = "wejście chybienia pamięci podręcznej";
"output" = "wynik";
Expand Down
Loading
Loading