Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
116 changes: 116 additions & 0 deletions Sources/CodexBar/CodexCostCatchUpPolicy.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import Foundation
import IOKit.ps

enum CodexCostCatchUpMode: String, Sendable {
case automatic
case accelerated
}

enum CodexCostCatchUpPowerSource: String, Sendable {
case ac
case battery
case unknown

static func current() -> Self {
guard let info = IOPSCopyPowerSourcesInfo()?.takeRetainedValue(),
let source = IOPSGetProvidingPowerSourceType(info)?.takeUnretainedValue() as String?
else {
return .unknown
}
if source == kIOPSACPowerValue as String {
return .ac
}
if source == kIOPSBatteryPowerValue as String {
return .battery
}
return .unknown
}
}

enum CodexCostCatchUpPauseReason: Sendable, Equatable {
case lowPower
case thermal
case user
case noProgress
case error(String)
}

struct CodexCostCatchUpActivity: Sendable, Equatable {
enum Phase: Sendable, Equatable {
case indexing
case paused
case complete
}

let phase: Phase
let mode: CodexCostCatchUpMode
let processedBytes: Int64
let totalBytes: Int64
let completedFiles: Int
let totalFiles: Int
let pauseReason: CodexCostCatchUpPauseReason?
let staleSnapshotUpdatedAt: Date?

var fractionCompleted: Double? {
guard self.totalBytes > 0 else {
guard self.totalFiles > 0 else { return nil }
return min(1, max(0, Double(self.completedFiles) / Double(self.totalFiles)))
}
return min(1, max(0, Double(self.processedBytes) / Double(self.totalBytes)))
}
}

struct CodexCostCatchUpPolicy: Sendable {
struct Input: Sendable {
let mode: CodexCostCatchUpMode
let previousActiveDuration: TimeInterval?
let powerSource: CodexCostCatchUpPowerSource
let lowPowerModeEnabled: Bool
let thermalState: ProcessInfo.ThermalState
}

struct Decision: Sendable, Equatable {
enum Action: Sendable, Equatable {
case runAfter(TimeInterval)
case pause(TimeInterval, CodexCostCatchUpPauseReason)
}

let action: Action
let targetDutyCycle: Double?
}

static let automaticBurstDuration: TimeInterval = 2
static let constrainedRetryDelay: TimeInterval = 60

func decision(for input: Input) -> Decision {
if input.thermalState == .critical {
return Decision(
action: .pause(Self.constrainedRetryDelay, .thermal),
targetDutyCycle: nil)
}
if input.mode == .automatic {
if input.lowPowerModeEnabled {
return Decision(
action: .pause(Self.constrainedRetryDelay, .lowPower),
targetDutyCycle: nil)
}
if input.thermalState == .serious {
return Decision(
action: .pause(Self.constrainedRetryDelay, .thermal),
targetDutyCycle: nil)
}
}
if input.mode == .accelerated {
return Decision(action: .runAfter(0), targetDutyCycle: 1)
}

let dutyCycle = switch input.powerSource {
case .ac: 0.20
case .battery: 0.05
case .unknown: 0.15
}
let activeDuration = max(0, input.previousActiveDuration ?? Self.automaticBurstDuration)
let delay = activeDuration * (1 - dutyCycle) / dutyCycle
return Decision(action: .runAfter(delay), targetDutyCycle: dutyCycle)
}
}
125 changes: 125 additions & 0 deletions Sources/CodexBar/PreferencesSpendDashboardPane.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,23 @@ func spendDashboardCoverageText(covered: Int, requested: Int) -> String {
"\(L("Coverage")): \(codexBarLocalizedInteger(covered)) / \(codexBarLocalizedInteger(requested))"
}

func codexCostCatchUpProgressText(_ activity: CodexCostCatchUpActivity) -> String {
if activity.totalBytes > 0 {
let processed = ByteCountFormatter.string(
fromByteCount: activity.processedBytes,
countStyle: .file)
let total = ByteCountFormatter.string(
fromByteCount: activity.totalBytes,
countStyle: .file)
return "\(processed) / \(total)"
}
if activity.totalFiles > 0 {
return "\(codexBarLocalizedInteger(activity.completedFiles)) / "
+ codexBarLocalizedInteger(activity.totalFiles)
}
return L("Loading…")
}

enum SpendDashboardModelHistoryPresentation: Equatable {
case unavailable
case empty
Expand Down Expand Up @@ -61,6 +78,7 @@ struct SpendDashboardPane: View {
ScrollView {
VStack(alignment: .leading, spacing: 18) {
self.header
self.codexCostCatchUpPanel

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 Bind catch-up controls to the dashboard account cache

When an account-scoped dashboard scan remains pending—for example, a Codex archive exceeding the 512 MiB per-refresh limit—this panel reports and controls UsageStore.codexCostCatchUpActivity, but dashboard rows are loaded from separate caches under accounts/<cacheIdentity> in SpendDashboardSource.load (SpendDashboardController.swift:273-283). The buttons therefore advance the store's default selected-account cache rather than the cache supplying the displayed row, and completing that task does not invalidate Codex dashboard data because Codex is excluded from sourceRevisions; users can click “Finish now” yet keep seeing the stale/partial dashboard result. Track catch-up per dashboard account cache or avoid presenting these controls as dashboard progress.

Useful? React with 👍 / 👎.

self.content
self.provenance
self.shareAction
Expand Down Expand Up @@ -124,6 +142,113 @@ struct SpendDashboardPane: View {
}
}

@ViewBuilder
private var codexCostCatchUpPanel: some View {
if let activity = self.store.codexCostCatchUpActivity,
activity.phase != .complete
{
SpendDashboardPanel {
VStack(alignment: .leading, spacing: 10) {
HStack(spacing: 8) {
Label(
self.codexCostCatchUpTitle(activity),
systemImage: activity.phase == .paused ? "pause.circle" : "externaldrive")
.font(.headline)
Spacer()
Text(codexCostCatchUpProgressText(activity))
.font(.caption.monospacedDigit())
.foregroundStyle(.secondary)
}

if let progress = activity.fractionCompleted {
ProgressView(value: progress)
} else if activity.phase == .indexing {
ProgressView()
.controlSize(.small)
}

if let staleSnapshotUpdatedAt = activity.staleSnapshotUpdatedAt {
HStack(spacing: 6) {
Label(L("stale data"), systemImage: "clock.badge.exclamationmark")
Text(L(
"Updated relative %@",
staleSnapshotUpdatedAt.relativeDescription()))
}
.font(.caption.weight(.medium))
.foregroundStyle(.orange)
}

Text(self.codexCostCatchUpDetail(activity))
.font(.caption)
.foregroundStyle(.secondary)

HStack {
if activity.pauseReason == .user
|| activity.pauseReason == .noProgress
|| self.codexCostCatchUpHasError(activity)
{
Button(L("Refresh")) {
self.store.returnCodexCostCatchUpToBackground()
}
} else if activity.mode == .automatic {
Button(L("Finish now")) {
self.store.startAcceleratedCodexCostCatchUp()
}
} else {
Button(L("Continue in background")) {
self.store.returnCodexCostCatchUpToBackground()
}
}

if activity.pauseReason != .user,
activity.pauseReason != .noProgress,
!self.codexCostCatchUpHasError(activity)
{
Button(L("Cancel")) {
self.store.stopCodexCostCatchUp()
}
}
}
.controlSize(.small)
}
}
}
}

private func codexCostCatchUpHasError(_ activity: CodexCostCatchUpActivity) -> Bool {
if case .error = activity.pauseReason {
return true
}
return false
}

private func codexCostCatchUpTitle(_ activity: CodexCostCatchUpActivity) -> String {
let prefix = L("Local estimated history")
switch activity.phase {
case .indexing:
return "\(prefix) · \(L("Refreshing"))"
case .paused:
return "\(prefix) · \(L("Inactive"))"
case .complete:
return "\(prefix) · \(L("Done"))"
}
}

private func codexCostCatchUpDetail(_ activity: CodexCostCatchUpActivity) -> String {
switch activity.pauseReason {
case .lowPower:
L("Battery Saver")
case .thermal, .user:
L("Inactive")
case .noProgress:
L("Error")
case let .error(message):
L("cost_status_error", L("Cost"), message)
case nil:
L("Estimated from local Codex logs for the selected account.")
}
}

@ViewBuilder
private var content: some View {
if !self.settings.costUsageEnabled {
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 @@ -1353,3 +1353,5 @@
"Cost today unavailable" = "تكلفة اليوم: غير متوفر";
"30-day cost unavailable" = "تكلفة 30 يوماً: غير متوفر";
"Resets" = "إعادات الضبط";
"Finish now" = "إنهاء الآن";
"Continue in background" = "المتابعة في الخلفية";
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 @@ -1352,3 +1352,5 @@
"Cost today unavailable" = "Cost d’avui: No disponible";
"30-day cost unavailable" = "Cost de 30 dies: No disponible";
"Resets" = "Reinicis";
"Finish now" = "Finalitza ara";
"Continue in background" = "Continua en segon pla";
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 @@ -1350,3 +1350,5 @@
"Cost today unavailable" = "Kosten heute: Nicht verfügbar";
"30-day cost unavailable" = "Kosten 30 Tage: Nicht verfügbar";
"Resets" = "Zurücksetzungen";
"Finish now" = "Jetzt abschließen";
"Continue in background" = "Im Hintergrund fortfahren";
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 @@ -1354,3 +1354,5 @@
"Cost today unavailable" = "Cost today unavailable";
"30-day cost unavailable" = "30-day cost unavailable";
"Resets" = "Resets";
"Finish now" = "Finish now";
"Continue in background" = "Continue in background";
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 @@ -1348,3 +1348,5 @@
"Cost today unavailable" = "Coste de hoy: No disponible";
"30-day cost unavailable" = "Coste de 30 días: No disponible";
"Resets" = "Reinicios";
"Finish now" = "Finalizar ahora";
"Continue in background" = "Continuar en segundo plano";
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 @@ -1353,3 +1353,5 @@
"Cost today unavailable" = "هزینهٔ امروز: در دسترس نیست";
"30-day cost unavailable" = "هزینهٔ ۳۰ روز: در دسترس نیست";
"Resets" = "بازنشانی‌ها";
"Finish now" = "اکنون تمام شود";
"Continue in background" = "ادامه در پس‌زمینه";
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 @@ -1349,3 +1349,5 @@
"Cost today unavailable" = "Coût aujourd’hui: Indisponible";
"30-day cost unavailable" = "Coût sur 30 j: Indisponible";
"Resets" = "Réinitialisations";
"Finish now" = "Terminer maintenant";
"Continue in background" = "Continuer en arrière-plan";
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 @@ -1349,3 +1349,5 @@
"Cost today unavailable" = "Custo de hoxe: Non dispoñible";
"30-day cost unavailable" = "Custo de 30 días: Non dispoñible";
"Resets" = "Reinicios";
"Finish now" = "Rematar agora";
"Continue in background" = "Continuar en segundo plano";
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 @@ -1353,3 +1353,5 @@
"Cost today unavailable" = "Biaya hari ini: Tidak tersedia";
"30-day cost unavailable" = "Biaya 30 hari: Tidak tersedia";
"Resets" = "Reset";
"Finish now" = "Selesaikan sekarang";
"Continue in background" = "Lanjutkan di latar belakang";
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 @@ -1353,3 +1353,5 @@
"Cost today unavailable" = "Costo oggi: Non disponibile";
"30-day cost unavailable" = "Costo 30 gg: Non disponibile";
"Resets" = "Ripristini";
"Finish now" = "Completa ora";
"Continue in background" = "Continua in background";
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 @@ -1350,3 +1350,5 @@
"Cost today unavailable" = "今日のコスト: 利用不可";
"30-day cost unavailable" = "30日間のコスト: 利用不可";
"Resets" = "リセット";
"Finish now" = "今すぐ完了";
"Continue in background" = "バックグラウンドで続行";
2 changes: 2 additions & 0 deletions Sources/CodexBar/Resources/ko.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1317,3 +1317,5 @@
"Cost today unavailable" = "오늘 비용: 사용할 수 없음";
"30-day cost unavailable" = "30일 비용: 사용할 수 없음";
"Resets" = "재설정";
"Finish now" = "지금 완료";
"Continue in background" = "백그라운드에서 계속";
2 changes: 2 additions & 0 deletions Sources/CodexBar/Resources/nl.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1349,3 +1349,5 @@
"Cost today unavailable" = "Kosten vandaag: Niet beschikbaar";
"30-day cost unavailable" = "Kosten 30 dagen: Niet beschikbaar";
"Resets" = "Resets";
"Finish now" = "Nu voltooien";
"Continue in background" = "Doorgaan op de achtergrond";
2 changes: 2 additions & 0 deletions Sources/CodexBar/Resources/pl.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1353,3 +1353,5 @@
"Cost today unavailable" = "Koszt dzisiaj: Niedostępne";
"30-day cost unavailable" = "Koszt 30 dni: Niedostępne";
"Resets" = "Resety";
"Finish now" = "Zakończ teraz";
"Continue in background" = "Kontynuuj w tle";
2 changes: 2 additions & 0 deletions Sources/CodexBar/Resources/pt-BR.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1350,3 +1350,5 @@
"Cost today unavailable" = "Custo hoje: Indisponível";
"30-day cost unavailable" = "Custo em 30 dias: Indisponível";
"Resets" = "Redefinições";
"Finish now" = "Concluir agora";
"Continue in background" = "Continuar em segundo plano";
2 changes: 2 additions & 0 deletions Sources/CodexBar/Resources/ru.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1351,3 +1351,5 @@
"Cost today unavailable" = "Расход сегодня: Недоступно";
"30-day cost unavailable" = "Расход за 30 дней: Недоступно";
"Resets" = "Сбросы";
"Finish now" = "Завершить сейчас";
"Continue in background" = "Продолжить в фоновом режиме";
2 changes: 2 additions & 0 deletions Sources/CodexBar/Resources/sv.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1348,3 +1348,5 @@
"Cost today unavailable" = "Kostnad idag: Inte tillgänglig";
"30-day cost unavailable" = "Kostnad 30 dagar: Inte tillgänglig";
"Resets" = "Återställningar";
"Finish now" = "Slutför nu";
"Continue in background" = "Fortsätt i bakgrunden";
2 changes: 2 additions & 0 deletions Sources/CodexBar/Resources/th.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1353,3 +1353,5 @@
"Cost today unavailable" = "ค่าใช้จ่ายวันนี้: ไม่พร้อมใช้งาน";
"30-day cost unavailable" = "ค่าใช้จ่าย 30 วัน: ไม่พร้อมใช้งาน";
"Resets" = "การรีเซ็ต";
"Finish now" = "เสร็จสิ้นตอนนี้";
"Continue in background" = "ทำต่อในเบื้องหลัง";
2 changes: 2 additions & 0 deletions Sources/CodexBar/Resources/tr.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1351,3 +1351,5 @@
"Cost today unavailable" = "Bugünkü maliyet: Kullanılamıyor";
"30-day cost unavailable" = "30 günlük maliyet: Kullanılamıyor";
"Resets" = "Sıfırlamalar";
"Finish now" = "Şimdi tamamla";
"Continue in background" = "Arka planda devam et";
2 changes: 2 additions & 0 deletions Sources/CodexBar/Resources/uk.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1349,3 +1349,5 @@
"Cost today unavailable" = "Вартість сьогодні: Недоступний";
"30-day cost unavailable" = "Вартість за 30 днів: Недоступний";
"Resets" = "Скидання";
"Finish now" = "Завершити зараз";
"Continue in background" = "Продовжити у фоновому режимі";
2 changes: 2 additions & 0 deletions Sources/CodexBar/Resources/vi.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1350,3 +1350,5 @@
"Cost today unavailable" = "Chi phí hôm nay: Không có sẵn";
"30-day cost unavailable" = "Chi phí 30 ngày: Không có sẵn";
"Resets" = "Lần đặt lại";
"Finish now" = "Hoàn tất ngay";
"Continue in background" = "Tiếp tục trong nền";
2 changes: 2 additions & 0 deletions Sources/CodexBar/Resources/zh-Hans.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1325,3 +1325,5 @@
"Cost today unavailable" = "今日费用: 不可用";
"30-day cost unavailable" = "30 天费用: 不可用";
"Resets" = "重置";
"Finish now" = "立即完成";
"Continue in background" = "转为后台运行";
Loading
Loading