Add Usage Metrics
Nucleic-Session: B4F51091-BE3C-42D3-BCDD-8E89E83543D0 Co-authored-by: Nucleic <[email protected]>
This commit is contained in:
@@ -330,39 +330,73 @@ struct QuotaCard: View {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Codex usage card (placeholder)
|
||||
// MARK: - Codex usage card
|
||||
|
||||
/// A placeholder counterpart to `QuotaCard` for Codex (GPT) subscription limits. Codex has
|
||||
/// no usage endpoint wired up yet, so this card only reserves the shape: the same card chrome
|
||||
/// and 5-hour / weekly rows as the Claude card, dimmed, with a "Coming soon" note. When the
|
||||
/// Codex usage backend lands it can render live windows here exactly like `QuotaCard`.
|
||||
/// The Codex (ChatGPT-subscription) counterpart to `QuotaCard`: the same card chrome and
|
||||
/// labeled window bars, reading `AppStore.codexUsage` — the rolling limits the `codex`
|
||||
/// app-server pushes during a turn (`account/rateLimits/updated`). Codex reports two windows
|
||||
/// (`primary` ≈ 5-hour, `secondary` ≈ weekly); each renders through the shared `UsageWindowRow`
|
||||
/// so it reads identically to the Claude card. Until a Codex turn has reported usage there's
|
||||
/// nothing to poll, so it falls back to a dimmed placeholder with a "no data yet" note.
|
||||
struct CodexUsageCard: View {
|
||||
@Environment(AppStore.self) private var store
|
||||
@Environment(\.appPalette) private var palette
|
||||
/// When > 0, stretch to at least this height (set to the paired card's height so a row of
|
||||
/// cards lines up — mirrors `QuotaCard.matchHeight`).
|
||||
var matchHeight: CGFloat = 0
|
||||
var title: String = "Codex"
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
HStack(spacing: 6) {
|
||||
Image(systemName: "gauge.with.dots.needle.67percent").foregroundStyle(.secondary)
|
||||
Text("Codex").font(.headline)
|
||||
Spacer()
|
||||
Text("Coming soon")
|
||||
.font(.caption2.weight(.semibold))
|
||||
.padding(.horizontal, 6).padding(.vertical, 2)
|
||||
.background(AppTheme.hairline, in: .capsule)
|
||||
// Re-evaluate every 30s so the "resets in" countdown stays current, matching `QuotaCard`.
|
||||
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(title).font(.headline)
|
||||
}
|
||||
content(now: context.date)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .topLeading)
|
||||
.padding(16)
|
||||
.frame(minHeight: matchHeight > 0 ? matchHeight : nil, alignment: .top)
|
||||
.background(AppTheme.surface, in: .rect(cornerRadius: 12))
|
||||
.overlay(RoundedRectangle(cornerRadius: 12).strokeBorder(AppTheme.hairline, lineWidth: 1))
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func content(now: Date) -> some View {
|
||||
if let usage = store.codexUsage, usage.hasData {
|
||||
VStack(alignment: .leading, spacing: 14) {
|
||||
windowRow(usage.primary, fallbackLabel: "5-hour limit", icon: QuotaFormat.fiveHourIcon, now: now)
|
||||
windowRow(usage.secondary, fallbackLabel: "Weekly limit", icon: QuotaFormat.weeklyIcon, now: now)
|
||||
}
|
||||
} else {
|
||||
VStack(alignment: .leading, spacing: 14) {
|
||||
placeholderRow("5-hour limit", icon: QuotaFormat.fiveHourIcon)
|
||||
placeholderRow("Weekly limit", icon: QuotaFormat.weeklyIcon)
|
||||
}
|
||||
.opacity(0.5)
|
||||
Text("Codex usage limits will appear here once support lands.")
|
||||
Text("Codex usage appears once a Codex chat reports it.")
|
||||
.font(.caption).foregroundStyle(.secondary)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .topLeading)
|
||||
.padding(16)
|
||||
.background(AppTheme.surface, in: .rect(cornerRadius: 12))
|
||||
.overlay(RoundedRectangle(cornerRadius: 12).strokeBorder(AppTheme.hairline, lineWidth: 1))
|
||||
}
|
||||
|
||||
/// A dimmed, dataless mirror of `QuotaCard.windowRow`: the label and an empty bar, with the
|
||||
/// One live Codex window; renders nothing for an absent window. Labels itself from the
|
||||
/// window's reported length when codex sends one, else the positional fallback.
|
||||
@ViewBuilder
|
||||
private func windowRow(
|
||||
_ window: CodexUsageWindow?, fallbackLabel: String, icon: String, now: Date
|
||||
) -> some View {
|
||||
if let window {
|
||||
UsageWindowRow(
|
||||
label: window.label ?? fallbackLabel, icon: icon,
|
||||
utilization: window.utilization(at: now),
|
||||
resetCaption: QuotaFormat.resetCaption(window.resetsAt, now: now), palette: palette)
|
||||
}
|
||||
}
|
||||
|
||||
/// A dimmed, dataless mirror of `UsageWindowRow`: the label and an empty bar, with the
|
||||
/// percentage shown as "—" so the row's shape reads without implying a real value.
|
||||
private func placeholderRow(_ label: String, icon: String) -> some View {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
|
||||
Reference in New Issue
Block a user