Merge nucleic/quiet-meadow-dingo-4rxh into dev
This commit is contained in:
@@ -3375,7 +3375,8 @@ final class RemoteStore: ObservableObject {
|
|||||||
dashboard = DashboardSnapshot(
|
dashboard = DashboardSnapshot(
|
||||||
counts: dashboard.counts, activity: dashboard.activity, projects: dashboard.projects,
|
counts: dashboard.counts, activity: dashboard.activity, projects: dashboard.projects,
|
||||||
todos: transform(dashboard.todos), usage: dashboard.usage,
|
todos: transform(dashboard.todos), usage: dashboard.usage,
|
||||||
codexUsage: dashboard.codexUsage, statusFeeds: dashboard.statusFeeds)
|
codexUsage: dashboard.codexUsage, grokUsage: dashboard.grokUsage,
|
||||||
|
statusFeeds: dashboard.statusFeeds)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -3458,6 +3459,7 @@ extension DashboardSnapshot {
|
|||||||
|
|
||||||
let usage = snaps.compactMap(\.usage).first
|
let usage = snaps.compactMap(\.usage).first
|
||||||
let codexUsage = snaps.compactMap(\.codexUsage).first
|
let codexUsage = snaps.compactMap(\.codexUsage).first
|
||||||
|
let grokUsage = snaps.compactMap(\.grokUsage).first
|
||||||
|
|
||||||
var seenProviders = Set<String>()
|
var seenProviders = Set<String>()
|
||||||
var statusFeeds: [WireStatusFeed] = []
|
var statusFeeds: [WireStatusFeed] = []
|
||||||
@@ -3469,7 +3471,8 @@ extension DashboardSnapshot {
|
|||||||
|
|
||||||
return DashboardSnapshot(
|
return DashboardSnapshot(
|
||||||
counts: counts, activity: activity, projects: projects,
|
counts: counts, activity: activity, projects: projects,
|
||||||
todos: todos, usage: usage, codexUsage: codexUsage, statusFeeds: statusFeeds)
|
todos: todos, usage: usage, codexUsage: codexUsage, grokUsage: grokUsage,
|
||||||
|
statusFeeds: statusFeeds)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -82,9 +82,11 @@ struct HomeView: View {
|
|||||||
statCards
|
statCards
|
||||||
|
|
||||||
// The Mac's usage gauges (5-hour / weekly windows); hidden when the host
|
// The Mac's usage gauges (5-hour / weekly windows); hidden when the host
|
||||||
// doesn't project usage. Codex's card follows, shown once a Codex chat reports.
|
// doesn't project usage. Codex's card follows, shown once a Codex chat
|
||||||
|
// reports, then Grok's once the host's poll finds a grok login.
|
||||||
QuotaCard(usage: store.dashboard.usage)
|
QuotaCard(usage: store.dashboard.usage)
|
||||||
CodexQuotaCard(usage: store.dashboard.codexUsage)
|
CodexQuotaCard(usage: store.dashboard.codexUsage)
|
||||||
|
GrokQuotaCard(usage: store.dashboard.grokUsage)
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 10) {
|
VStack(alignment: .leading, spacing: 10) {
|
||||||
Text("Activity").font(.headline)
|
Text("Activity").font(.headline)
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ import NucleicProtocol
|
|||||||
enum QuotaFormat {
|
enum QuotaFormat {
|
||||||
static let fiveHourIcon = "clock"
|
static let fiveHourIcon = "clock"
|
||||||
static let weeklyIcon = "calendar"
|
static let weeklyIcon = "calendar"
|
||||||
|
/// Grok bills a credit allowance rather than a time window, so its rows read as credits.
|
||||||
|
static let creditsIcon = "creditcard"
|
||||||
|
static let onDemandIcon = "plus.forwardslash.minus"
|
||||||
|
|
||||||
static func percent(_ value: Double) -> String { "\(Int(value.rounded()))%" }
|
static func percent(_ value: Double) -> String { "\(Int(value.rounded()))%" }
|
||||||
|
|
||||||
@@ -168,6 +171,72 @@ struct CodexQuotaCard: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Grok quota card (the Mac's GrokUsageCard, dashboard form)
|
||||||
|
|
||||||
|
/// The Grok counterpart to `QuotaCard`, rendered from `DashboardSnapshot.grokUsage` — the
|
||||||
|
/// billing period's included credit allowance the host polls from grok's chat proxy. Grok bills
|
||||||
|
/// credits rather than time windows, so the primary row is the period's allowance; the
|
||||||
|
/// on-demand overage row joins it only for accounts that have on-demand billing enabled with a
|
||||||
|
/// cap. Renders nothing until the host has a grok login to poll, so Claude-only users never
|
||||||
|
/// carry an empty card. Same thresholds, icons, and copy as the Mac's card.
|
||||||
|
struct GrokQuotaCard: View {
|
||||||
|
let usage: GrokUsage?
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
if let usage {
|
||||||
|
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("Grok").font(.headline)
|
||||||
|
}
|
||||||
|
row(label: usage.cycleLabel, icon: QuotaFormat.creditsIcon,
|
||||||
|
utilization: usage.utilization(at: context.date),
|
||||||
|
resetsAt: usage.resetsAt, now: context.date)
|
||||||
|
if let onDemand = usage.onDemandUtilization(at: context.date) {
|
||||||
|
row(label: "On-demand", icon: QuotaFormat.onDemandIcon,
|
||||||
|
utilization: onDemand,
|
||||||
|
resetsAt: usage.resetsAt, now: context.date)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
}
|
||||||
|
.card()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func row(
|
||||||
|
label: String, icon: String, utilization: Double, resetsAt: Date?, now: Date
|
||||||
|
) -> some View {
|
||||||
|
let color = QuotaFormat.color(forPercent: utilization)
|
||||||
|
return VStack(alignment: .leading, spacing: 4) {
|
||||||
|
HStack(spacing: 5) {
|
||||||
|
Image(systemName: icon).font(.caption).foregroundStyle(.secondary)
|
||||||
|
Text(label).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(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)
|
// MARK: - Status pill (the Mac's StatusFeedIndicator)
|
||||||
|
|
||||||
/// A compact service-status pill: a calm green check while every watched provider is
|
/// A compact service-status pill: a calm green check while every watched provider is
|
||||||
|
|||||||
Reference in New Issue
Block a user