codex-flow 2.1.13__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. codex_flow/__init__.py +28 -0
  2. codex_flow/__main__.py +9 -0
  3. codex_flow/cli.py +242 -0
  4. codex_flow/data/LICENSE +21 -0
  5. codex_flow/data/README.en.md +303 -0
  6. codex_flow/data/README.md +305 -0
  7. codex_flow/data/VERSION +1 -0
  8. codex_flow/data/apps/chatgpt-mcp/README.md +86 -0
  9. codex_flow/data/apps/chatgpt-mcp/__init__.py +1 -0
  10. codex_flow/data/apps/chatgpt-mcp/adapter.py +458 -0
  11. codex_flow/data/apps/chatgpt-mcp/server.py +358 -0
  12. codex_flow/data/apps/chatgpt-mcp/widget.html +927 -0
  13. codex_flow/data/apps/macos-overlay/README.en.md +121 -0
  14. codex_flow/data/apps/macos-overlay/README.md +123 -0
  15. codex_flow/data/apps/macos-overlay/Sources/Controllers/OverlayRuntimeState.swift +126 -0
  16. codex_flow/data/apps/macos-overlay/Sources/Controllers/OverlayScreenGeometry.swift +82 -0
  17. codex_flow/data/apps/macos-overlay/Sources/Controllers/OverlayWindowController.swift +1052 -0
  18. codex_flow/data/apps/macos-overlay/Sources/Localization.swift +197 -0
  19. codex_flow/data/apps/macos-overlay/Sources/Models/TelemetryData.swift +1557 -0
  20. codex_flow/data/apps/macos-overlay/Sources/Services/AccountSnapshotService.swift +1101 -0
  21. codex_flow/data/apps/macos-overlay/Sources/Services/FlowPilotInstanceLock.swift +153 -0
  22. codex_flow/data/apps/macos-overlay/Sources/Services/IPCServer.swift +298 -0
  23. codex_flow/data/apps/macos-overlay/Sources/Services/TelemetryQueryEngine.swift +800 -0
  24. codex_flow/data/apps/macos-overlay/Sources/Services/TelemetryWatcher.swift +135 -0
  25. codex_flow/data/apps/macos-overlay/Sources/Services/UpdateService.swift +610 -0
  26. codex_flow/data/apps/macos-overlay/Sources/Views/AccountView.swift +610 -0
  27. codex_flow/data/apps/macos-overlay/Sources/Views/AnalyticsView.swift +566 -0
  28. codex_flow/data/apps/macos-overlay/Sources/Views/AutostartView.swift +293 -0
  29. codex_flow/data/apps/macos-overlay/Sources/Views/BubbleView.swift +317 -0
  30. codex_flow/data/apps/macos-overlay/Sources/Views/HistoryView.swift +1124 -0
  31. codex_flow/data/apps/macos-overlay/Sources/Views/HoverRevealText.swift +165 -0
  32. codex_flow/data/apps/macos-overlay/Sources/Views/InspectorSkillsToolsView.swift +121 -0
  33. codex_flow/data/apps/macos-overlay/Sources/Views/LogoView.swift +182 -0
  34. codex_flow/data/apps/macos-overlay/Sources/Views/SleekSwitch.swift +117 -0
  35. codex_flow/data/apps/macos-overlay/Sources/Views/StrategyModeView.swift +561 -0
  36. codex_flow/data/apps/macos-overlay/Sources/Views/SummaryView.swift +1273 -0
  37. codex_flow/data/apps/macos-overlay/Sources/Views/UpdateView.swift +352 -0
  38. codex_flow/data/apps/macos-overlay/Sources/main.swift +340 -0
  39. codex_flow/data/apps/macos-overlay/Tests/OverlayScreenGeometryTests.swift +163 -0
  40. codex_flow/data/apps/macos-overlay/Tests/TelemetryPhase1ContractTests.swift +357 -0
  41. codex_flow/data/apps/macos-overlay/Tests/TelemetryQueryEngineConcurrencyTests.swift +221 -0
  42. codex_flow/data/apps/macos-overlay/Tests/TelemetryQuotaSelectionTests.swift +158 -0
  43. codex_flow/data/apps/macos-overlay/Tests/TelemetryWorkerTokenTests.swift +122 -0
  44. codex_flow/data/apps/macos-overlay/build.sh +75 -0
  45. codex_flow/data/benchmark/corpus.json +103 -0
  46. codex_flow/data/benchmark/manifest.example.json +41 -0
  47. codex_flow/data/benchmark/manifest.schema.json +137 -0
  48. codex_flow/data/benchmark/prices/gpt-5.6-2026-08-30.json +5 -0
  49. codex_flow/data/benchmark/profiles.json +90 -0
  50. codex_flow/data/benchmark/schema.json +77 -0
  51. codex_flow/data/benchmark/tasks.json +50 -0
  52. codex_flow/data/completions/codex-flow.bash +34 -0
  53. codex_flow/data/completions/codex-flow.zsh +52 -0
  54. codex_flow/data/glama.json +6 -0
  55. codex_flow/data/install-release.ps1 +126 -0
  56. codex_flow/data/install-release.sh +155 -0
  57. codex_flow/data/install.ps1 +349 -0
  58. codex_flow/data/install.sh +362 -0
  59. codex_flow/data/policy/benchmark.toml +49 -0
  60. codex_flow/data/policy/defaults.toml +70 -0
  61. codex_flow/data/scripts/analyze-benchmark.py +510 -0
  62. codex_flow/data/scripts/benchmark-local.py +171 -0
  63. codex_flow/data/scripts/check-recommendation.py +277 -0
  64. codex_flow/data/scripts/doctor.py +449 -0
  65. codex_flow/data/scripts/generate-release-manifest.py +74 -0
  66. codex_flow/data/scripts/localization.py +192 -0
  67. codex_flow/data/scripts/manage-hooks.py +448 -0
  68. codex_flow/data/scripts/manage-instructions.py +389 -0
  69. codex_flow/data/scripts/manage-shell.py +151 -0
  70. codex_flow/data/scripts/materialize-corpus.py +193 -0
  71. codex_flow/data/scripts/menu.py +646 -0
  72. codex_flow/data/scripts/migrations/0001_update_settings.py +80 -0
  73. codex_flow/data/scripts/package-release.py +132 -0
  74. codex_flow/data/scripts/render-benchmark-report.py +292 -0
  75. codex_flow/data/scripts/run-benchmark.py +829 -0
  76. codex_flow/data/scripts/strategies/__init__.py +28 -0
  77. codex_flow/data/scripts/strategies/balanced.py +115 -0
  78. codex_flow/data/scripts/strategies/base.py +363 -0
  79. codex_flow/data/scripts/strategies/efficient.py +158 -0
  80. codex_flow/data/scripts/strategies/lifecycle_runtime.py +590 -0
  81. codex_flow/data/scripts/strategies/quality.py +209 -0
  82. codex_flow/data/scripts/strategies/speed.py +108 -0
  83. codex_flow/data/scripts/strategies/task_budget_runtime.py +644 -0
  84. codex_flow/data/scripts/strategies/task_phase_runtime.py +341 -0
  85. codex_flow/data/scripts/strategies/work_unit_runtime.py +421 -0
  86. codex_flow/data/scripts/strategy_runtime.py +1091 -0
  87. codex_flow/data/scripts/telemetry.py +400 -0
  88. codex_flow/data/scripts/telemetry_core/__init__.py +192 -0
  89. codex_flow/data/scripts/telemetry_core/app_server.py +1192 -0
  90. codex_flow/data/scripts/telemetry_core/collector.py +1247 -0
  91. codex_flow/data/scripts/telemetry_core/common.py +421 -0
  92. codex_flow/data/scripts/telemetry_core/latency.py +593 -0
  93. codex_flow/data/scripts/telemetry_core/query.py +427 -0
  94. codex_flow/data/scripts/telemetry_core/quota_ledger.py +598 -0
  95. codex_flow/data/scripts/telemetry_core/render.py +460 -0
  96. codex_flow/data/scripts/telemetry_core/repair.py +223 -0
  97. codex_flow/data/scripts/ui.py +266 -0
  98. codex_flow/data/scripts/update-homebrew-formula.py +146 -0
  99. codex_flow/data/scripts/update_runtime_config.py +134 -0
  100. codex_flow/data/scripts/updater.py +1718 -0
  101. codex_flow/data/smithery.yaml +18 -0
  102. codex_flow/data/templates/agents/worker-explorer.toml +24 -0
  103. codex_flow/data/templates/agents/worker-implementer.toml +49 -0
  104. codex_flow/data/templates/agents/worker-reviewer.toml +25 -0
  105. codex_flow/data/templates/flow-pilot-instructions.md +35 -0
  106. codex_flow/data/templates/skills/flow-pilot/SKILL.md +577 -0
  107. codex_flow/mcp.py +35 -0
  108. codex_flow-2.1.13.dist-info/METADATA +342 -0
  109. codex_flow-2.1.13.dist-info/RECORD +113 -0
  110. codex_flow-2.1.13.dist-info/WHEEL +5 -0
  111. codex_flow-2.1.13.dist-info/entry_points.txt +3 -0
  112. codex_flow-2.1.13.dist-info/licenses/LICENSE +21 -0
  113. codex_flow-2.1.13.dist-info/top_level.txt +1 -0
@@ -0,0 +1,566 @@
1
+ import SwiftUI
2
+
3
+ public struct AnalyticsView: View {
4
+ @ObservedObject var state: OverlayState
5
+ @ObservedObject private var localization = AppLocalization.shared
6
+ public var isFullHeight: Bool = false
7
+
8
+ @State private var trendRuns: [TaskRun] = []
9
+ @State private var trendLoading = false
10
+ @State private var trendRequestGeneration = 0
11
+
12
+ public init(state: OverlayState, isFullHeight: Bool = false) {
13
+ self.state = state
14
+ self.isFullHeight = isFullHeight
15
+ }
16
+
17
+ private var stats: TelemetryStats {
18
+ state.statsData ?? TelemetryStats()
19
+ }
20
+
21
+ public var body: some View {
22
+ let content = VStack(spacing: 9) {
23
+ periodHeader
24
+
25
+ if stats.totalRuns == 0 {
26
+ emptyStatsState
27
+ } else {
28
+ kpiSummaryGrid
29
+ usageTrendCard
30
+ efficiencyCard
31
+
32
+ if !stats.models.isEmpty {
33
+ modelBreakdownCard
34
+ }
35
+
36
+ if !stats.projects.isEmpty {
37
+ projectBreakdownCard
38
+ }
39
+ }
40
+ }
41
+ .padding(.vertical, 7)
42
+
43
+ return Group {
44
+ if isFullHeight {
45
+ content
46
+ } else {
47
+ ScrollView(.vertical, showsIndicators: true) {
48
+ content
49
+ }
50
+ .frame(maxHeight: 405)
51
+ }
52
+ }
53
+ .onAppear {
54
+ state.loadStats()
55
+ loadTrendRuns()
56
+ }
57
+ .onChange(of: state.statsDays) { _, _ in
58
+ loadTrendRuns()
59
+ }
60
+ .onChange(of: state.selectedProject) { _, _ in
61
+ loadTrendRuns()
62
+ }
63
+ }
64
+
65
+ // MARK: - Period
66
+
67
+ private var periodHeader: some View {
68
+ HStack {
69
+ VStack(alignment: .leading, spacing: 1) {
70
+ Text(L("Aggregation Summary", "聚合统计"))
71
+ .font(.system(size: 10.5, weight: .bold, design: .rounded))
72
+ .foregroundColor(.white.opacity(0.86))
73
+ Text(L("Deterministic telemetry only", "仅展示确定性遥测数据"))
74
+ .font(.system(size: 7.5))
75
+ .foregroundColor(.white.opacity(0.36))
76
+ }
77
+
78
+ Spacer()
79
+
80
+ HStack(spacing: 2) {
81
+ dayButton(days: 7, title: L("7 Days", "7 天"))
82
+ dayButton(days: 30, title: L("30 Days", "30 天"))
83
+ }
84
+ .padding(2)
85
+ .background(Capsule().fill(Color.white.opacity(0.06)))
86
+ }
87
+ }
88
+
89
+ private func dayButton(days: Int, title: String) -> some View {
90
+ Button {
91
+ withAnimation(.easeInOut(duration: 0.14)) {
92
+ state.statsDays = days
93
+ state.loadStats()
94
+ }
95
+ } label: {
96
+ Text(title)
97
+ .font(.system(size: 8.4, weight: state.statsDays == days ? .bold : .medium, design: .rounded))
98
+ .foregroundColor(state.statsDays == days ? .white : .white.opacity(0.48))
99
+ .padding(.horizontal, 7)
100
+ .padding(.vertical, 3)
101
+ .background(Capsule().fill(state.statsDays == days ? Color.cyan.opacity(0.28) : Color.clear))
102
+ }
103
+ .buttonStyle(.plain)
104
+ }
105
+
106
+ // MARK: - Empty
107
+
108
+ private var emptyStatsState: some View {
109
+ VStack(spacing: 8) {
110
+ Image(systemName: "chart.xyaxis.line")
111
+ .font(.system(size: 24))
112
+ .foregroundColor(.white.opacity(0.28))
113
+ .padding(.top, 28)
114
+ Text(L("No activity in this period", "当前周期暂无活动"))
115
+ .font(.system(size: 11, weight: .semibold))
116
+ .foregroundColor(.white.opacity(0.72))
117
+ Text(L("Task, token and efficiency trends will appear after telemetry is recorded.", "记录遥测后会显示任务、Token 与效率趋势。"))
118
+ .font(.system(size: 8.8))
119
+ .foregroundColor(.white.opacity(0.4))
120
+ .multilineTextAlignment(.center)
121
+ .padding(.horizontal, 15)
122
+ }
123
+ .frame(maxWidth: .infinity, minHeight: 225)
124
+ }
125
+
126
+ // MARK: - KPIs
127
+
128
+ private var kpiSummaryGrid: some View {
129
+ HStack(spacing: 6) {
130
+ analyticsKPI(
131
+ title: L("Tasks", "任务"),
132
+ value: "\(stats.totalRuns)",
133
+ subtext: L("\(stats.delegatedRuns) delegated", "委派 \(stats.delegatedRuns) 次"),
134
+ icon: "sparkles",
135
+ accent: .cyan
136
+ )
137
+
138
+ analyticsKPI(
139
+ title: L("Active Time", "活跃时长"),
140
+ value: stats.formattedTotalDuration,
141
+ subtext: L("past \(stats.days)d", "近 \(stats.days) 天"),
142
+ icon: "timer",
143
+ accent: .indigo
144
+ )
145
+
146
+ // No monetary "Cost" here: token counts are deterministic, while
147
+ // estimated credits are optional and are not equivalent to USD.
148
+ analyticsKPI(
149
+ title: L("Tokens", "Token"),
150
+ value: TaskRun.formatTokenCount(stats.totalTokens),
151
+ subtext: L(
152
+ "out \(TaskRun.formatTokenCount(stats.outputTokens))",
153
+ "输出 \(TaskRun.formatTokenCount(stats.outputTokens))"
154
+ ),
155
+ icon: "circle.grid.cross.fill",
156
+ accent: Color(red: 0.95, green: 0.35, blue: 0.8)
157
+ )
158
+ }
159
+ }
160
+
161
+ private func analyticsKPI(title: String, value: String, subtext: String, icon: String, accent: Color) -> some View {
162
+ VStack(spacing: 2.5) {
163
+ HStack(spacing: 3) {
164
+ Image(systemName: icon)
165
+ .font(.system(size: 7.5))
166
+ .foregroundColor(accent)
167
+ Text(title)
168
+ .font(.system(size: 7.8, weight: .medium, design: .rounded))
169
+ .foregroundColor(.white.opacity(0.46))
170
+ .textCase(.uppercase)
171
+ }
172
+
173
+ Text(value)
174
+ .font(.system(size: 12, weight: .bold, design: .rounded))
175
+ .foregroundColor(.white)
176
+ .lineLimit(1)
177
+
178
+ Text(subtext)
179
+ .font(.system(size: 7.3, design: .rounded))
180
+ .foregroundColor(.white.opacity(0.4))
181
+ .lineLimit(1)
182
+ }
183
+ .frame(maxWidth: .infinity)
184
+ .padding(.vertical, 6)
185
+ .background(analyticsCardBackground)
186
+ }
187
+
188
+ // MARK: - Trend curve
189
+
190
+ private var usageTrendCard: some View {
191
+ let points = UsageTrendPoint.make(runs: trendRuns, days: state.statsDays)
192
+ return VStack(alignment: .leading, spacing: 5) {
193
+ HStack {
194
+ HStack(spacing: 4) {
195
+ Image(systemName: "chart.xyaxis.line")
196
+ .font(.system(size: 8))
197
+ .foregroundColor(.cyan)
198
+ Text(L("Usage Trend", "用量趋势"))
199
+ .font(.system(size: 8.8, weight: .bold, design: .rounded))
200
+ .foregroundColor(.white.opacity(0.72))
201
+ }
202
+ Spacer()
203
+ if trendLoading {
204
+ ProgressView().controlSize(.mini)
205
+ } else {
206
+ Text(L("tokens / day", "Token / 天"))
207
+ .font(.system(size: 7.2))
208
+ .foregroundColor(.white.opacity(0.34))
209
+ }
210
+ }
211
+
212
+ UsageTrendChart(points: points)
213
+ .frame(height: 104)
214
+ }
215
+ .padding(8)
216
+ .background(analyticsCardBackground)
217
+ }
218
+
219
+ // MARK: - Efficiency
220
+
221
+ private var efficiencyCard: some View {
222
+ VStack(spacing: 7) {
223
+ efficiencyRow(
224
+ title: L("Cache Efficiency", "缓存效率"),
225
+ percentage: stats.cacheRatio,
226
+ detail: L("\(TaskRun.formatTokenCount(stats.cachedInputTokens)) cached", "缓存 \(TaskRun.formatTokenCount(stats.cachedInputTokens))"),
227
+ accent: Color(red: 0.28, green: 0.58, blue: 0.95)
228
+ )
229
+
230
+ efficiencyRow(
231
+ title: L("Worker Offload", "Worker 分流"),
232
+ percentage: stats.workerOffloadRatio,
233
+ detail: "\(TaskRun.formatTokenCount(stats.workerTokens)) / \(TaskRun.formatTokenCount(stats.totalTokens))",
234
+ accent: Color(red: 0.22, green: 0.88, blue: 0.58)
235
+ )
236
+ }
237
+ .padding(8)
238
+ .background(analyticsCardBackground)
239
+ }
240
+
241
+ private func efficiencyRow(title: String, percentage: Double, detail: String, accent: Color) -> some View {
242
+ VStack(alignment: .leading, spacing: 3) {
243
+ HStack {
244
+ Text(title)
245
+ .font(.system(size: 8.4, weight: .medium))
246
+ .foregroundColor(.white.opacity(0.7))
247
+ Spacer()
248
+ Text(String(format: "%.1f%%", percentage))
249
+ .font(.system(size: 8.5, weight: .bold, design: .rounded))
250
+ .foregroundColor(accent)
251
+ Text("(\(detail))")
252
+ .font(.system(size: 7.2))
253
+ .foregroundColor(.white.opacity(0.35))
254
+ }
255
+
256
+ GeometryReader { proxy in
257
+ ZStack(alignment: .leading) {
258
+ Capsule().fill(Color.white.opacity(0.075))
259
+ Capsule()
260
+ .fill(accent)
261
+ .frame(width: proxy.size.width * CGFloat(max(0, min(1, percentage / 100.0))))
262
+ }
263
+ }
264
+ .frame(height: 4.5)
265
+ }
266
+ }
267
+
268
+ // MARK: - Model & project breakdown
269
+
270
+ private var modelBreakdownCard: some View {
271
+ VStack(alignment: .leading, spacing: 5) {
272
+ Text(L("Model Breakdown", "模型分布"))
273
+ .font(.system(size: 8.6, weight: .bold, design: .rounded))
274
+ .foregroundColor(.white.opacity(0.62))
275
+ .textCase(.uppercase)
276
+
277
+ ForEach(stats.models) { model in
278
+ AnalyticsModelRow(model: model, totalTokens: stats.totalTokens)
279
+ }
280
+ }
281
+ .padding(8)
282
+ .background(analyticsCardBackground)
283
+ }
284
+
285
+ private var projectBreakdownCard: some View {
286
+ VStack(alignment: .leading, spacing: 5) {
287
+ Text(L("Projects Distribution", "项目分布"))
288
+ .font(.system(size: 8.6, weight: .bold, design: .rounded))
289
+ .foregroundColor(.white.opacity(0.62))
290
+ .textCase(.uppercase)
291
+
292
+ ForEach(Array(stats.projects.prefix(6))) { project in
293
+ AnalyticsProjectRow(project: project, isPrivacyMode: state.isPrivacyMode)
294
+ }
295
+ }
296
+ .padding(8)
297
+ .background(analyticsCardBackground)
298
+ }
299
+
300
+ private var analyticsCardBackground: some View {
301
+ RoundedRectangle(cornerRadius: 9)
302
+ .fill(Color.white.opacity(0.035))
303
+ .overlay(RoundedRectangle(cornerRadius: 9).stroke(Color.white.opacity(0.07), lineWidth: 0.7))
304
+ }
305
+
306
+ private func loadTrendRuns() {
307
+ trendRequestGeneration += 1
308
+ let generation = trendRequestGeneration
309
+ trendLoading = true
310
+ let days = state.statsDays
311
+ let project = state.selectedProject
312
+ DispatchQueue.global(qos: .userInitiated).async {
313
+ let runs = TelemetryQueryEngine.shared.fetchHistory(
314
+ limit: 1200,
315
+ project: project,
316
+ todayOnly: false,
317
+ search: ""
318
+ )
319
+ let cutoff = Date().addingTimeInterval(-Double(days) * 86_400).timeIntervalSince1970 * 1000.0
320
+ let filtered = runs.filter { ($0.startedAtMs ?? 0) >= cutoff }
321
+ DispatchQueue.main.async {
322
+ guard generation == trendRequestGeneration else { return }
323
+ trendRuns = filtered
324
+ trendLoading = false
325
+ }
326
+ }
327
+ }
328
+ }
329
+
330
+ // MARK: - Trend chart
331
+
332
+ public struct UsageTrendPoint: Identifiable {
333
+ public var id: Date { date }
334
+ public let date: Date
335
+ public let tokens: Int
336
+ public let runs: Int
337
+
338
+ public static func make(runs: [TaskRun], days: Int) -> [UsageTrendPoint] {
339
+ let calendar = Calendar.current
340
+ let end = calendar.startOfDay(for: Date())
341
+ let start = calendar.date(byAdding: .day, value: -(max(1, days) - 1), to: end) ?? end
342
+
343
+ var buckets: [Date: (tokens: Int, runs: Int)] = [:]
344
+ for run in runs {
345
+ guard let ms = run.startedAtMs else { continue }
346
+ let day = calendar.startOfDay(for: Date(timeIntervalSince1970: ms / 1000.0))
347
+ guard day >= start && day <= end else { continue }
348
+ let existing = buckets[day] ?? (0, 0)
349
+ buckets[day] = (existing.tokens + (run.aggregatedUsage.totalTokens ?? 0), existing.runs + 1)
350
+ }
351
+
352
+ return (0..<max(1, days)).compactMap { offset in
353
+ guard let day = calendar.date(byAdding: .day, value: offset, to: start) else { return nil }
354
+ let value = buckets[day] ?? (0, 0)
355
+ return UsageTrendPoint(date: day, tokens: value.tokens, runs: value.runs)
356
+ }
357
+ }
358
+ }
359
+
360
+ public struct UsageTrendChart: View {
361
+ public let points: [UsageTrendPoint]
362
+
363
+ public init(points: [UsageTrendPoint]) {
364
+ self.points = points
365
+ }
366
+
367
+ public var body: some View {
368
+ let maxTokens = max(1, points.map(\.tokens).max() ?? 0)
369
+ VStack(spacing: 2) {
370
+ HStack(spacing: 5) {
371
+ VStack {
372
+ Text(TaskRun.formatTokenCount(maxTokens))
373
+ .font(.system(size: 6.7, design: .monospaced))
374
+ .foregroundColor(.white.opacity(0.3))
375
+ Spacer()
376
+ Text("0")
377
+ .font(.system(size: 6.7, design: .monospaced))
378
+ .foregroundColor(.white.opacity(0.3))
379
+ }
380
+ .frame(width: 28)
381
+
382
+ GeometryReader { proxy in
383
+ ZStack {
384
+ VStack(spacing: 0) {
385
+ chartGridLine
386
+ Spacer()
387
+ chartGridLine
388
+ Spacer()
389
+ chartGridLine
390
+ }
391
+
392
+ if points.count > 1 {
393
+ trendPath(size: proxy.size, maxTokens: maxTokens)
394
+ .stroke(
395
+ LinearGradient(colors: [.cyan, .indigo], startPoint: .leading, endPoint: .trailing),
396
+ style: StrokeStyle(lineWidth: 2, lineCap: .round, lineJoin: .round)
397
+ )
398
+ .shadow(color: .cyan.opacity(0.3), radius: 3)
399
+
400
+ areaPath(size: proxy.size, maxTokens: maxTokens)
401
+ .fill(
402
+ LinearGradient(
403
+ colors: [.cyan.opacity(0.16), .cyan.opacity(0.0)],
404
+ startPoint: .top,
405
+ endPoint: .bottom
406
+ )
407
+ )
408
+ }
409
+
410
+ ForEach(Array(points.enumerated()), id: \.element.id) { index, point in
411
+ if point.tokens > 0 && (points.count <= 8 || index % max(1, points.count / 7) == 0 || index == points.count - 1) {
412
+ Circle()
413
+ .fill(Color.cyan)
414
+ .frame(width: 4, height: 4)
415
+ .position(position(index: index, tokens: point.tokens, size: proxy.size, maxTokens: maxTokens))
416
+ .help("\(formatTrendDate(point.date)) · \(TaskRun.formatTokenCount(point.tokens)) tokens · \(point.runs) tasks")
417
+ }
418
+ }
419
+ }
420
+ }
421
+ }
422
+
423
+ HStack {
424
+ Text(points.first.map { formatTrendDate($0.date) } ?? "—")
425
+ Spacer()
426
+ Text(points.last.map { formatTrendDate($0.date) } ?? "—")
427
+ }
428
+ .font(.system(size: 6.8, design: .monospaced))
429
+ .foregroundColor(.white.opacity(0.3))
430
+ .padding(.leading, 33)
431
+ }
432
+ }
433
+
434
+ private var chartGridLine: some View {
435
+ Rectangle()
436
+ .fill(Color.white.opacity(0.055))
437
+ .frame(height: 0.5)
438
+ }
439
+
440
+ private func position(index: Int, tokens: Int, size: CGSize, maxTokens: Int) -> CGPoint {
441
+ let denominator = max(1, points.count - 1)
442
+ let x = size.width * CGFloat(index) / CGFloat(denominator)
443
+ let yRatio = CGFloat(Double(tokens) / Double(maxTokens))
444
+ let y = size.height - (size.height * yRatio)
445
+ return CGPoint(x: x, y: max(1, min(size.height - 1, y)))
446
+ }
447
+
448
+ private func trendPath(size: CGSize, maxTokens: Int) -> Path {
449
+ var path = Path()
450
+ guard points.count > 1 else { return path }
451
+ let positions = points.enumerated().map { position(index: $0.offset, tokens: $0.element.tokens, size: size, maxTokens: maxTokens) }
452
+ path.move(to: positions[0])
453
+ for index in 1..<positions.count {
454
+ let previous = positions[index - 1]
455
+ let current = positions[index]
456
+ let midpoint = (previous.x + current.x) / 2
457
+ path.addCurve(
458
+ to: current,
459
+ control1: CGPoint(x: midpoint, y: previous.y),
460
+ control2: CGPoint(x: midpoint, y: current.y)
461
+ )
462
+ }
463
+ return path
464
+ }
465
+
466
+ private func areaPath(size: CGSize, maxTokens: Int) -> Path {
467
+ var path = trendPath(size: size, maxTokens: maxTokens)
468
+ guard points.count > 1 else { return path }
469
+ path.addLine(to: CGPoint(x: size.width, y: size.height))
470
+ path.addLine(to: CGPoint(x: 0, y: size.height))
471
+ path.closeSubpath()
472
+ return path
473
+ }
474
+ }
475
+
476
+ private func formatTrendDate(_ date: Date) -> String {
477
+ let formatter = DateFormatter()
478
+ formatter.dateFormat = "MM-dd"
479
+ return formatter.string(from: date)
480
+ }
481
+
482
+ // MARK: - Breakdown rows
483
+
484
+ public struct AnalyticsModelRow: View {
485
+ public let model: ModelStats
486
+ public let totalTokens: Int
487
+
488
+ public var body: some View {
489
+ let percentage = totalTokens > 0 ? Double(model.tokens) / Double(totalTokens) * 100 : 0
490
+ HStack(spacing: 4) {
491
+ HoverRevealText(
492
+ model.name,
493
+ font: .system(size: 8.8, weight: .semibold, design: .monospaced),
494
+ foregroundColor: .white.opacity(0.86),
495
+ lineLimit: 1,
496
+ popoverWidth: 320
497
+ )
498
+ .frame(maxWidth: 115, alignment: .leading)
499
+
500
+ HStack(spacing: 2) {
501
+ ForEach(model.roles, id: \.self) { role in
502
+ Text(localizedRole(role))
503
+ .font(.system(size: 6.8, weight: .medium))
504
+ .foregroundColor(role == "parent" ? .indigo : .teal)
505
+ .padding(.horizontal, 3)
506
+ .padding(.vertical, 1)
507
+ .background(Capsule().fill((role == "parent" ? Color.indigo : Color.teal).opacity(0.13)))
508
+ }
509
+ }
510
+
511
+ Spacer(minLength: 2)
512
+
513
+ Text(L("\(model.calls) calls", "\(model.calls) 次"))
514
+ .font(.system(size: 7.2))
515
+ .foregroundColor(.white.opacity(0.35))
516
+
517
+ Text(TaskRun.formatTokenCount(model.tokens))
518
+ .font(.system(size: 8.2, weight: .bold, design: .rounded))
519
+ .foregroundColor(Color(red: 0.95, green: 0.35, blue: 0.8))
520
+ .frame(minWidth: 38, alignment: .trailing)
521
+
522
+ Text(String(format: "%.0f%%", percentage))
523
+ .font(.system(size: 7.2, design: .rounded))
524
+ .foregroundColor(.white.opacity(0.4))
525
+ .frame(width: 25, alignment: .trailing)
526
+ }
527
+ .padding(.horizontal, 6)
528
+ .padding(.vertical, 3.5)
529
+ .background(RoundedRectangle(cornerRadius: 6).fill(Color.white.opacity(0.022)))
530
+ }
531
+ }
532
+
533
+ public struct AnalyticsProjectRow: View {
534
+ public let project: ProjectStats
535
+ public let isPrivacyMode: Bool
536
+
537
+ public var body: some View {
538
+ HStack(spacing: 5) {
539
+ Image(systemName: "folder.fill")
540
+ .font(.system(size: 7))
541
+ .foregroundColor(.white.opacity(0.42))
542
+
543
+ HoverRevealText(
544
+ project.name,
545
+ font: .system(size: 8.7, weight: .medium, design: .rounded),
546
+ foregroundColor: .white.opacity(0.8),
547
+ lineLimit: 1,
548
+ privacyBlur: isPrivacyMode,
549
+ popoverWidth: 340
550
+ )
551
+
552
+ Spacer(minLength: 3)
553
+
554
+ Text(L("\(project.runs) runs", "\(project.runs) 次"))
555
+ .font(.system(size: 7.2))
556
+ .foregroundColor(.white.opacity(0.34))
557
+
558
+ Text(TaskRun.formatTokenCount(project.tokens))
559
+ .font(.system(size: 8, weight: .bold, design: .rounded))
560
+ .foregroundColor(Color(red: 0.95, green: 0.35, blue: 0.8))
561
+ }
562
+ .padding(.horizontal, 6)
563
+ .padding(.vertical, 3.5)
564
+ .background(RoundedRectangle(cornerRadius: 6).fill(Color.white.opacity(0.022)))
565
+ }
566
+ }