diff --git a/NucleicRemote/NucleicRemote/Views/StatusAndQuota.swift b/NucleicRemote/NucleicRemote/Views/StatusAndQuota.swift index 588e591..50a2c57 100644 --- a/NucleicRemote/NucleicRemote/Views/StatusAndQuota.swift +++ b/NucleicRemote/NucleicRemote/Views/StatusAndQuota.swift @@ -105,6 +105,70 @@ struct QuotaCard: View { } } +// MARK: - Codex quota card (the Mac's CodexUsageCard, dashboard form) + +/// The Codex counterpart to `QuotaCard`, rendered from `DashboardSnapshot.codexUsage` — the +/// rolling windows the host's `codex` agent reported (`primary` ≈ 5-hour, `secondary` ≈ weekly). +/// Renders nothing until a Codex chat has reported usage, so Claude-only users never carry an +/// empty card. Same thresholds, icons, and copy as the Claude card. +struct CodexQuotaCard: View { + let usage: CodexUsage? + + var body: some View { + if let usage, usage.hasData { + TimelineView(.periodic(from: .now, by: 30)) { context in + VStack(alignment: .leading, spacing: 12) { + HStack(spacing: 6) { + Image(systemName: "gauge.with.dots.needle.67percent").foregroundStyle(.secondary) + Text("Codex").font(.headline) + } + windowRow(usage.primary, fallbackLabel: "5-hour limit", + icon: QuotaFormat.fiveHourIcon, now: context.date) + windowRow(usage.secondary, fallbackLabel: "Weekly limit", + icon: QuotaFormat.weeklyIcon, now: context.date) + } + .frame(maxWidth: .infinity, alignment: .leading) + } + .card() + } + } + + @ViewBuilder + private func windowRow( + _ window: CodexUsageWindow?, fallbackLabel: String, icon: String, now: Date + ) -> some View { + if let window { + let utilization = window.utilization(at: now) + let color = QuotaFormat.color(forPercent: utilization) + VStack(alignment: .leading, spacing: 4) { + HStack(spacing: 5) { + Image(systemName: icon).font(.caption).foregroundStyle(.secondary) + Text(window.label ?? fallbackLabel).font(.subheadline) + Spacer() + Text(QuotaFormat.percent(utilization)) + .font(.subheadline.monospacedDigit().weight(.semibold)) + .foregroundStyle(color) + } + bar(fraction: utilization / 100, color: color) + if let caption = QuotaFormat.resetCaption(window.resetsAt, now: now) { + Text(caption).font(.caption2).foregroundStyle(.secondary) + } + } + } + } + + private func bar(fraction: Double, color: Color) -> some View { + GeometryReader { geo in + ZStack(alignment: .leading) { + Capsule().fill(Color.secondary.opacity(0.18)) + Capsule().fill(color) + .frame(width: max(3, geo.size.width * min(1, max(0, fraction)))) + } + } + .frame(height: 6) + } +} + // MARK: - Status pill (the Mac's StatusFeedIndicator) /// A compact service-status pill: a calm green check while every watched provider is