From a83d635b6be5b7e70097efd42bbac679dde24852 Mon Sep 17 00:00:00 2001 From: Andrew Blakeslee Moore Date: Thu, 9 Jul 2026 01:45:24 -0700 Subject: [PATCH 1/8] Add Usage Metrics Nucleic-Session: B4F51091-BE3C-42D3-BCDD-8E89E83543D0 Co-authored-by: Nucleic --- .../NucleicRemote/Views/Transcript/TranscriptProjection.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/NucleicRemote/NucleicRemote/Views/Transcript/TranscriptProjection.swift b/NucleicRemote/NucleicRemote/Views/Transcript/TranscriptProjection.swift index 6b41b81..f53cb91 100644 --- a/NucleicRemote/NucleicRemote/Views/Transcript/TranscriptProjection.swift +++ b/NucleicRemote/NucleicRemote/Views/Transcript/TranscriptProjection.swift @@ -478,6 +478,8 @@ enum TranscriptProjection { items.append(.init(id: "usage-\(event.seq)", seq: event.seq, kind: .usage(usage))) case .rateLimit(let limit): items.append(.init(id: "rate-\(event.seq)", seq: event.seq, kind: .rateLimit(limit))) + case .codexUsage: + break // account-wide quota telemetry, folded into the dashboard — not the transcript case .turnCompleted: items.append(.init(id: "turn-\(event.seq)", seq: event.seq, kind: .turnBoundary)) case .approvalRequested(let req): From fbba0f5f60cfbb221b9729b601fd5ac1cabfd9bf Mon Sep 17 00:00:00 2001 From: Andrew Blakeslee Moore Date: Thu, 9 Jul 2026 01:45:36 -0700 Subject: [PATCH 2/8] Add Usage Metrics Nucleic-Session: B4F51091-BE3C-42D3-BCDD-8E89E83543D0 Co-authored-by: Nucleic --- .../Views/Transcript/IncrementalTranscriptProjection.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NucleicRemote/NucleicRemote/Views/Transcript/IncrementalTranscriptProjection.swift b/NucleicRemote/NucleicRemote/Views/Transcript/IncrementalTranscriptProjection.swift index 40ffb82..b702694 100644 --- a/NucleicRemote/NucleicRemote/Views/Transcript/IncrementalTranscriptProjection.swift +++ b/NucleicRemote/NucleicRemote/Views/Transcript/IncrementalTranscriptProjection.swift @@ -263,7 +263,7 @@ final class IncrementalTranscriptProjection { if seenToolItem.insert(c.toolCallID).inserted { creations[r] = .tool } case .toolResult(let result): spans[result.toolCallID]?.resultSeen = true - case .toolCallInputDelta, .fileChange, .approvalResolved: + case .toolCallInputDelta, .fileChange, .approvalResolved, .codexUsage: break // refs (if any) tracked above; creates nothing case .turnCompleted, .runFinished: creations[r] = .separator From 375f999e5c687c1ef35c933450efb02ccc50ab09 Mon Sep 17 00:00:00 2001 From: Andrew Blakeslee Moore Date: Thu, 9 Jul 2026 01:47:24 -0700 Subject: [PATCH 3/8] Add Usage Metrics Nucleic-Session: B4F51091-BE3C-42D3-BCDD-8E89E83543D0 Co-authored-by: Nucleic --- NucleicRemote/NucleicRemote/Models/RemoteStore.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/NucleicRemote/NucleicRemote/Models/RemoteStore.swift b/NucleicRemote/NucleicRemote/Models/RemoteStore.swift index e116d6e..b70ad10 100644 --- a/NucleicRemote/NucleicRemote/Models/RemoteStore.swift +++ b/NucleicRemote/NucleicRemote/Models/RemoteStore.swift @@ -1910,6 +1910,7 @@ extension DashboardSnapshot { tokens: snaps.reduce(0) { $0 + $1.counts.tokens }) let usage = snaps.compactMap(\.usage).first + let codexUsage = snaps.compactMap(\.codexUsage).first var seenProviders = Set() var statusFeeds: [WireStatusFeed] = [] From 7c2ead6c097436aeacd4eea82894344e0d92c6f9 Mon Sep 17 00:00:00 2001 From: Andrew Blakeslee Moore Date: Thu, 9 Jul 2026 01:47:30 -0700 Subject: [PATCH 4/8] Add Usage Metrics Nucleic-Session: B4F51091-BE3C-42D3-BCDD-8E89E83543D0 Co-authored-by: Nucleic --- NucleicRemote/NucleicRemote/Models/RemoteStore.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NucleicRemote/NucleicRemote/Models/RemoteStore.swift b/NucleicRemote/NucleicRemote/Models/RemoteStore.swift index b70ad10..2f369a4 100644 --- a/NucleicRemote/NucleicRemote/Models/RemoteStore.swift +++ b/NucleicRemote/NucleicRemote/Models/RemoteStore.swift @@ -1922,7 +1922,7 @@ extension DashboardSnapshot { return DashboardSnapshot( counts: counts, activity: activity, projects: projects, - todos: todos, usage: usage, statusFeeds: statusFeeds) + todos: todos, usage: usage, codexUsage: codexUsage, statusFeeds: statusFeeds) } } From cba241e4e922e4641939767b9ee587d8197bb21f Mon Sep 17 00:00:00 2001 From: Andrew Blakeslee Moore Date: Thu, 9 Jul 2026 01:47:35 -0700 Subject: [PATCH 5/8] Add Usage Metrics Nucleic-Session: B4F51091-BE3C-42D3-BCDD-8E89E83543D0 Co-authored-by: Nucleic --- NucleicRemote/NucleicRemote/Models/RemoteStore.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/NucleicRemote/NucleicRemote/Models/RemoteStore.swift b/NucleicRemote/NucleicRemote/Models/RemoteStore.swift index 2f369a4..cc7e44f 100644 --- a/NucleicRemote/NucleicRemote/Models/RemoteStore.swift +++ b/NucleicRemote/NucleicRemote/Models/RemoteStore.swift @@ -1828,7 +1828,8 @@ final class RemoteStore: ObservableObject { private func demoMutateTodos(_ transform: ([WireTodo]) -> [WireTodo]) { dashboard = DashboardSnapshot( counts: dashboard.counts, activity: dashboard.activity, projects: dashboard.projects, - todos: transform(dashboard.todos), usage: dashboard.usage, statusFeeds: dashboard.statusFeeds) + todos: transform(dashboard.todos), usage: dashboard.usage, + codexUsage: dashboard.codexUsage, statusFeeds: dashboard.statusFeeds) } From af972a55a202dc555c7a3b4b7e2079fd0236a14b Mon Sep 17 00:00:00 2001 From: Andrew Blakeslee Moore Date: Thu, 9 Jul 2026 01:50:25 -0700 Subject: [PATCH 6/8] Add Usage Metrics Nucleic-Session: B4F51091-BE3C-42D3-BCDD-8E89E83543D0 Co-authored-by: Nucleic --- NucleicRemote/NucleicRemote/Views/StatusAndQuota.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/NucleicRemote/NucleicRemote/Views/StatusAndQuota.swift b/NucleicRemote/NucleicRemote/Views/StatusAndQuota.swift index a73f198..588e591 100644 --- a/NucleicRemote/NucleicRemote/Views/StatusAndQuota.swift +++ b/NucleicRemote/NucleicRemote/Views/StatusAndQuota.swift @@ -26,7 +26,11 @@ enum QuotaFormat { } static func resetCaption(_ window: WireUsageWindow, now: Date) -> String? { - guard let resetsAt = window.resetsAt else { return nil } + resetCaption(window.resetsAt, now: now) + } + + static func resetCaption(_ resetsAt: Date?, now: Date) -> String? { + guard let resetsAt else { return nil } let remaining = resetsAt.timeIntervalSince(now) guard remaining > 0 else { return "resetting…" } return "resets in \(duration(remaining))" From d4c8eab3a714f5b693b815055acffb106bbd817c Mon Sep 17 00:00:00 2001 From: Andrew Blakeslee Moore Date: Thu, 9 Jul 2026 01:50:39 -0700 Subject: [PATCH 7/8] Add Usage Metrics Nucleic-Session: B4F51091-BE3C-42D3-BCDD-8E89E83543D0 Co-authored-by: Nucleic --- .../NucleicRemote/Views/StatusAndQuota.swift | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) 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 From 5f54c8a4e466db1896e4333b78c1aac1036b8f41 Mon Sep 17 00:00:00 2001 From: Andrew Blakeslee Moore Date: Thu, 9 Jul 2026 01:50:47 -0700 Subject: [PATCH 8/8] Add Usage Metrics Nucleic-Session: B4F51091-BE3C-42D3-BCDD-8E89E83543D0 Co-authored-by: Nucleic --- NucleicRemote/NucleicRemote/Views/HomeView.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/NucleicRemote/NucleicRemote/Views/HomeView.swift b/NucleicRemote/NucleicRemote/Views/HomeView.swift index 1c79eee..ab2aac9 100644 --- a/NucleicRemote/NucleicRemote/Views/HomeView.swift +++ b/NucleicRemote/NucleicRemote/Views/HomeView.swift @@ -82,8 +82,9 @@ struct HomeView: View { statCards // The Mac's usage gauges (5-hour / weekly windows); hidden when the host - // doesn't project usage. + // doesn't project usage. Codex's card follows, shown once a Codex chat reports. QuotaCard(usage: store.dashboard.usage) + CodexQuotaCard(usage: store.dashboard.codexUsage) VStack(alignment: .leading, spacing: 10) { Text("Activity").font(.headline)