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 @@ -681,7 +700,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 @@ -622,7 +622,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 @@ -904,6 +904,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 @@ -767,6 +767,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/de.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1182,6 +1182,13 @@
"Show Codex Spark usage" = "Codex-Spark-Nutzung anzeigen";
"Shows Codex Spark quota rows in the menu and provider preview. Requires optional credits and extra usage in Display settings." = "Zeigt die Codex-Spark-Kontingentzeilen im Menü und in der Anbietervorschau an. Erfordert, dass „Credits + zusätzliche Nutzung anzeigen“ in den Anzeigeeinstellungen aktiviert ist.";
"Scroll to see more models" = "Scrollen, um weitere Modelle zu sehen";
"DeepSeek this month token usage trend" = "Trend der DeepSeek-Token-Nutzung in diesem Monat";
"Chrome profile" = "Chrome-Profil";
"Choose which signed-in DeepSeek Platform session supplies detailed usage." = "Wähle aus, welche angemeldete DeepSeek-Platform-Sitzung detaillierte Nutzungsdaten liefert.";
"Detailed usage unavailable." = "Detaillierte Nutzung nicht verfügbar.";
"Sign in to DeepSeek Platform in Chrome for detailed usage." = "Melde dich für detaillierte Nutzungsdaten in Chrome bei DeepSeek Platform an.";
"Select a DeepSeek Chrome profile in Settings." = "Wähle in den Einstellungen ein DeepSeek-Chrome-Profil aus.";
"Select profile…" = "Profil auswählen…";

"%@: %@" = "%@: %@";
"Alternatively, set a custom path in Settings." = "Alternativ können Sie in den Einstellungen einen benutzerdefinierten Pfad festlegen.";
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 @@ -905,6 +905,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/es.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1043,6 +1043,13 @@
"Show Codex Spark usage" = "Mostrar el uso de Codex Spark";
"Shows Codex Spark quota rows in the menu and provider preview. Requires optional credits and extra usage in Display settings." = "Muestra las filas de cuota de Codex Spark en el menú y en la vista previa del proveedor. Requiere activar «Mostrar créditos + uso adicional» en los ajustes de Pantalla.";
"Scroll to see more models" = "Desplázate para ver más modelos";
"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." = "Elige qué sesión iniciada de DeepSeek Platform proporciona el uso detallado.";
"Detailed usage unavailable." = "El uso detallado no está disponible.";
"Sign in to DeepSeek Platform in Chrome for detailed usage." = "Inicia sesión en DeepSeek Platform en Chrome para ver el uso detallado.";
"Select a DeepSeek Chrome profile in Settings." = "Selecciona un perfil de Chrome de DeepSeek en Ajustes.";
"Select profile…" = "Seleccionar perfil…";

"%@ · %@" = "%@ · %@";
"%@ is unavailable in the current environment." = "%@ no está disponible en el entorno actual.";
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 @@ -904,6 +904,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/fr.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1182,6 +1182,13 @@
"Show Codex Spark usage" = "Afficher l’utilisation de Codex Spark";
"Shows Codex Spark quota rows in the menu and provider preview. Requires optional credits and extra usage in Display settings." = "Affiche les lignes de quota Codex Spark dans le menu et l’aperçu du fournisseur. Nécessite d’activer « Afficher les crédits + utilisation supplémentaire » dans les réglages Affichage.";
"Scroll to see more models" = "Faites défiler pour voir plus de modèles";
"DeepSeek this month token usage trend" = "Tendance d’utilisation des jetons DeepSeek ce mois-ci";
"Chrome profile" = "Profil Chrome";
"Choose which signed-in DeepSeek Platform session supplies detailed usage." = "Choisissez la session DeepSeek Platform connectée qui fournit l’utilisation détaillée.";
"Detailed usage unavailable." = "L’utilisation détaillée n’est pas disponible.";
"Sign in to DeepSeek Platform in Chrome for detailed usage." = "Connectez-vous à DeepSeek Platform dans Chrome pour afficher l’utilisation détaillée.";
"Select a DeepSeek Chrome profile in Settings." = "Sélectionnez un profil Chrome DeepSeek dans Réglages.";
"Select profile…" = "Sélectionner un profil…";

"%@: %@" = "%@ : %@";
"Alternatively, set a custom path in Settings." = "Vous pouvez également définir un chemin personnalisé dans Paramètres.";
Expand Down
Loading
Loading