-
Notifications
You must be signed in to change notification settings - Fork 876
Expand file tree
/
Copy pathCodexBarWidgetProvider.swift
More file actions
312 lines (275 loc) · 11.8 KB
/
CodexBarWidgetProvider.swift
File metadata and controls
312 lines (275 loc) · 11.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
import AppIntents
import CodexBarCore
import SwiftUI
import WidgetKit
enum ProviderChoice: String, AppEnum {
case codex
case claude
case gemini
case alibaba
case antigravity
case zai
case copilot
case minimax
case kilo
case opencode
case opencodego
static let typeDisplayRepresentation = TypeDisplayRepresentation(name: "Provider")
static let caseDisplayRepresentations: [ProviderChoice: DisplayRepresentation] = [
.codex: DisplayRepresentation(title: "Codex"),
.claude: DisplayRepresentation(title: "Claude"),
.gemini: DisplayRepresentation(title: "Gemini"),
.alibaba: DisplayRepresentation(title: "Alibaba"),
.antigravity: DisplayRepresentation(title: "Antigravity"),
.zai: DisplayRepresentation(title: "z.ai"),
.copilot: DisplayRepresentation(title: "Copilot"),
.minimax: DisplayRepresentation(title: "MiniMax"),
.kilo: DisplayRepresentation(title: "Kilo"),
.opencode: DisplayRepresentation(title: "OpenCode"),
.opencodego: DisplayRepresentation(title: "OpenCode Go"),
]
var provider: UsageProvider {
switch self {
case .codex: .codex
case .claude: .claude
case .gemini: .gemini
case .alibaba: .alibaba
case .antigravity: .antigravity
case .zai: .zai
case .copilot: .copilot
case .minimax: .minimax
case .kilo: .kilo
case .opencode: .opencode
case .opencodego: .opencodego
}
}
// swiftlint:disable:next cyclomatic_complexity
init?(provider: UsageProvider) {
switch provider {
case .codex: self = .codex
case .claude: self = .claude
case .gemini: self = .gemini
case .alibaba: self = .alibaba
case .antigravity: self = .antigravity
case .cursor: return nil // Cursor not yet supported in widgets
case .opencode: self = .opencode
case .opencodego: self = .opencodego
case .zai: self = .zai
case .factory: return nil // Factory not yet supported in widgets
case .copilot: self = .copilot
case .minimax: self = .minimax
case .vertexai: return nil // Vertex AI not yet supported in widgets
case .kilo: self = .kilo
case .kiro: return nil // Kiro not yet supported in widgets
case .augment: return nil // Augment not yet supported in widgets
case .jetbrains: return nil // JetBrains not yet supported in widgets
case .kimi: return nil // Kimi not yet supported in widgets
case .kimik2: return nil // Kimi K2 not yet supported in widgets
case .amp: return nil // Amp not yet supported in widgets
case .ollama: return nil // Ollama not yet supported in widgets
case .synthetic: return nil // Synthetic not yet supported in widgets
case .openrouter: return nil // OpenRouter not yet supported in widgets
case .warp: return nil // Warp not yet supported in widgets
case .perplexity: return nil // Perplexity not yet supported in widgets
case .abacus: return nil // Abacus AI not yet supported in widgets
case .mistral: return nil // Mistral not yet supported in widgets
case .deepseek: return nil // DeepSeek not yet supported in widgets
}
}
}
enum CompactMetric: String, AppEnum {
case credits
case todayCost
case last30DaysCost
static let typeDisplayRepresentation = TypeDisplayRepresentation(name: "Metric")
static let caseDisplayRepresentations: [CompactMetric: DisplayRepresentation] = [
.credits: DisplayRepresentation(title: "Credits left"),
.todayCost: DisplayRepresentation(title: "Today cost"),
.last30DaysCost: DisplayRepresentation(title: "30d cost"),
]
}
struct ProviderSelectionIntent: AppIntent, WidgetConfigurationIntent {
static let title: LocalizedStringResource = "Provider"
static let description = IntentDescription("Select the provider to display in the widget.")
@Parameter(title: "Provider", default: .codex)
var provider: ProviderChoice
init() {
self.provider = .codex
}
}
struct SwitchWidgetProviderIntent: AppIntent {
static let title: LocalizedStringResource = "Switch Provider"
static let description = IntentDescription("Switch the provider shown in the widget.")
@Parameter(title: "Provider")
var provider: ProviderChoice
init() {}
init(provider: ProviderChoice) {
self.provider = provider
}
func perform() async throws -> some IntentResult {
WidgetSelectionStore.saveSelectedProvider(self.provider.provider)
WidgetCenter.shared.reloadAllTimelines()
return .result()
}
}
struct CompactMetricSelectionIntent: AppIntent, WidgetConfigurationIntent {
static let title: LocalizedStringResource = "Provider + Metric"
static let description = IntentDescription("Select the provider and metric to display.")
@Parameter(title: "Provider", default: .codex)
var provider: ProviderChoice
@Parameter(title: "Metric", default: .credits)
var metric: CompactMetric
init() {
self.provider = .codex
self.metric = .credits
}
}
struct CodexBarWidgetEntry: TimelineEntry {
let date: Date
let provider: UsageProvider
let snapshot: WidgetSnapshot
}
struct CodexBarCompactEntry: TimelineEntry {
let date: Date
let provider: UsageProvider
let metric: CompactMetric
let snapshot: WidgetSnapshot
}
struct CodexBarSwitcherEntry: TimelineEntry {
let date: Date
let provider: UsageProvider
let availableProviders: [UsageProvider]
let snapshot: WidgetSnapshot
}
struct CodexBarTimelineProvider: AppIntentTimelineProvider {
func placeholder(in context: Context) -> CodexBarWidgetEntry {
CodexBarWidgetEntry(
date: Date(),
provider: .codex,
snapshot: WidgetPreviewData.snapshot())
}
func snapshot(for configuration: ProviderSelectionIntent, in context: Context) async -> CodexBarWidgetEntry {
let provider = configuration.provider.provider
return CodexBarWidgetEntry(
date: Date(),
provider: provider,
snapshot: WidgetSnapshotStore.load() ?? WidgetPreviewData.snapshot())
}
func timeline(
for configuration: ProviderSelectionIntent,
in context: Context) async -> Timeline<CodexBarWidgetEntry>
{
let provider = configuration.provider.provider
let snapshot = WidgetSnapshotStore.load() ?? WidgetPreviewData.emptySnapshot()
let entry = CodexBarWidgetEntry(date: Date(), provider: provider, snapshot: snapshot)
let refresh = Date().addingTimeInterval(30 * 60)
return Timeline(entries: [entry], policy: .after(refresh))
}
}
struct CodexBarSwitcherTimelineProvider: TimelineProvider {
func placeholder(in context: Context) -> CodexBarSwitcherEntry {
let snapshot = WidgetPreviewData.snapshot()
let providers = self.availableProviders(from: snapshot)
return CodexBarSwitcherEntry(
date: Date(),
provider: providers.first ?? .codex,
availableProviders: providers,
snapshot: snapshot)
}
func getSnapshot(in context: Context, completion: @escaping (CodexBarSwitcherEntry) -> Void) {
completion(self.makeEntry())
}
func getTimeline(in context: Context, completion: @escaping (Timeline<CodexBarSwitcherEntry>) -> Void) {
let entry = self.makeEntry()
let refresh = Date().addingTimeInterval(30 * 60)
completion(Timeline(entries: [entry], policy: .after(refresh)))
}
private func makeEntry() -> CodexBarSwitcherEntry {
let snapshot = WidgetSnapshotStore.load() ?? WidgetPreviewData.emptySnapshot()
let providers = self.availableProviders(from: snapshot)
let stored = WidgetSelectionStore.loadSelectedProvider()
let selected = providers.first { $0 == stored } ?? providers.first ?? .codex
if selected != stored {
WidgetSelectionStore.saveSelectedProvider(selected)
}
return CodexBarSwitcherEntry(
date: Date(),
provider: selected,
availableProviders: providers,
snapshot: snapshot)
}
private func availableProviders(from snapshot: WidgetSnapshot) -> [UsageProvider] {
Self.supportedProviders(from: snapshot)
}
static func supportedProviders(from snapshot: WidgetSnapshot) -> [UsageProvider] {
let enabled = snapshot.enabledProviders
let providers = enabled.isEmpty ? snapshot.entries.map(\.provider) : enabled
let supported = providers.filter { ProviderChoice(provider: $0) != nil }
return supported.isEmpty ? [.codex] : supported
}
}
struct CodexBarCompactTimelineProvider: AppIntentTimelineProvider {
func placeholder(in context: Context) -> CodexBarCompactEntry {
CodexBarCompactEntry(
date: Date(),
provider: .codex,
metric: .credits,
snapshot: WidgetPreviewData.snapshot())
}
func snapshot(for configuration: CompactMetricSelectionIntent, in context: Context) async -> CodexBarCompactEntry {
let provider = configuration.provider.provider
let metric = configuration.metric
return CodexBarCompactEntry(
date: Date(),
provider: provider,
metric: metric,
snapshot: WidgetSnapshotStore.load() ?? WidgetPreviewData.snapshot())
}
func timeline(
for configuration: CompactMetricSelectionIntent,
in context: Context) async -> Timeline<CodexBarCompactEntry>
{
let provider = configuration.provider.provider
let metric = configuration.metric
let snapshot = WidgetSnapshotStore.load() ?? WidgetPreviewData.emptySnapshot()
let entry = CodexBarCompactEntry(
date: Date(),
provider: provider,
metric: metric,
snapshot: snapshot)
let refresh = Date().addingTimeInterval(30 * 60)
return Timeline(entries: [entry], policy: .after(refresh))
}
}
enum WidgetPreviewData {
static func emptySnapshot() -> WidgetSnapshot {
WidgetSnapshot(entries: [], enabledProviders: [], generatedAt: Date())
}
static func snapshot() -> WidgetSnapshot {
let primary = RateWindow(usedPercent: 35, windowMinutes: nil, resetsAt: nil, resetDescription: "Resets in 4h")
let secondary = RateWindow(usedPercent: 60, windowMinutes: nil, resetsAt: nil, resetDescription: "Resets in 3d")
let entry = WidgetSnapshot.ProviderEntry(
provider: .codex,
updatedAt: Date(),
primary: primary,
secondary: secondary,
tertiary: nil,
creditsRemaining: 1243.4,
codeReviewRemainingPercent: 78,
tokenUsage: WidgetSnapshot.TokenUsageSummary(
sessionCostUSD: 12.4,
sessionTokens: 420_000,
last30DaysCostUSD: 923.8,
last30DaysTokens: 12_400_000),
dailyUsage: [
WidgetSnapshot.DailyUsagePoint(dayKey: "2025-12-01", totalTokens: 120_000, costUSD: 15.2),
WidgetSnapshot.DailyUsagePoint(dayKey: "2025-12-02", totalTokens: 80000, costUSD: 10.1),
WidgetSnapshot.DailyUsagePoint(dayKey: "2025-12-03", totalTokens: 140_000, costUSD: 17.9),
WidgetSnapshot.DailyUsagePoint(dayKey: "2025-12-04", totalTokens: 90000, costUSD: 11.4),
WidgetSnapshot.DailyUsagePoint(dayKey: "2025-12-05", totalTokens: 160_000, costUSD: 19.8),
WidgetSnapshot.DailyUsagePoint(dayKey: "2025-12-06", totalTokens: 70000, costUSD: 8.9),
WidgetSnapshot.DailyUsagePoint(dayKey: "2025-12-07", totalTokens: 110_000, costUSD: 13.7),
])
return WidgetSnapshot(entries: [entry], generatedAt: Date())
}
}