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,1557 @@
1
+ import Foundation
2
+
3
+ public enum OverlayTab: String, CaseIterable, Identifiable {
4
+ case inspector = "Inspector"
5
+ case history = "History"
6
+ case analytics = "Analytics"
7
+
8
+ public var id: String { rawValue }
9
+
10
+ public var iconName: String {
11
+ switch self {
12
+ case .inspector: return "bolt.fill"
13
+ case .history: return "clock.arrow.circlepath"
14
+ case .analytics: return "chart.bar.xaxis"
15
+ }
16
+ }
17
+ }
18
+
19
+ public struct GitInfo: Codable {
20
+ public var branch: String?
21
+ public var commit: String?
22
+
23
+ public init(branch: String? = nil, commit: String? = nil) {
24
+ self.branch = branch
25
+ self.commit = commit
26
+ }
27
+ }
28
+
29
+ public struct ThreadInfo: Codable {
30
+ public var name: String?
31
+ public var preview: String?
32
+ public var cwd: String?
33
+ public var gitInfo: GitInfo?
34
+
35
+ public init(name: String? = nil, preview: String? = nil, cwd: String? = nil, gitInfo: GitInfo? = nil) {
36
+ self.name = name
37
+ self.preview = preview
38
+ self.cwd = cwd
39
+ self.gitInfo = gitInfo
40
+ }
41
+ }
42
+
43
+ public struct TokenUsage: Codable {
44
+ public var totalTokens: Int?
45
+ public var promptTokens: Int?
46
+ public var inputTokens: Int?
47
+ public var completionTokens: Int?
48
+ public var outputTokens: Int?
49
+ public var cachedPromptTokens: Int?
50
+ public var cachedInputTokens: Int?
51
+ public var cacheReadInputTokens: Int?
52
+ public var cacheCreationInputTokens: Int?
53
+ public var reasoningOutputTokens: Int?
54
+ public var estimatedCreditsMicros: Double?
55
+
56
+ enum CodingKeys: String, CodingKey {
57
+ case totalTokens = "total_tokens"
58
+ case promptTokens = "prompt_tokens"
59
+ case inputTokens = "input_tokens"
60
+ case completionTokens = "completion_tokens"
61
+ case outputTokens = "output_tokens"
62
+ case cachedPromptTokens = "cached_prompt_tokens"
63
+ case cachedInputTokens = "cached_input_tokens"
64
+ case cacheReadInputTokens = "cache_read_input_tokens"
65
+ case cacheCreationInputTokens = "cache_creation_input_tokens"
66
+ case reasoningOutputTokens = "reasoning_output_tokens"
67
+ case estimatedCreditsMicros = "estimated_credits_micros"
68
+ }
69
+
70
+ public init(
71
+ totalTokens: Int? = nil,
72
+ promptTokens: Int? = nil,
73
+ inputTokens: Int? = nil,
74
+ completionTokens: Int? = nil,
75
+ outputTokens: Int? = nil,
76
+ cachedPromptTokens: Int? = nil,
77
+ cachedInputTokens: Int? = nil,
78
+ cacheReadInputTokens: Int? = nil,
79
+ cacheCreationInputTokens: Int? = nil,
80
+ reasoningOutputTokens: Int? = nil,
81
+ estimatedCreditsMicros: Double? = nil
82
+ ) {
83
+ self.totalTokens = totalTokens
84
+ self.promptTokens = promptTokens
85
+ self.inputTokens = inputTokens
86
+ self.completionTokens = completionTokens
87
+ self.outputTokens = outputTokens
88
+ self.cachedPromptTokens = cachedPromptTokens
89
+ self.cachedInputTokens = cachedInputTokens
90
+ self.cacheReadInputTokens = cacheReadInputTokens
91
+ self.cacheCreationInputTokens = cacheCreationInputTokens
92
+ self.reasoningOutputTokens = reasoningOutputTokens
93
+ self.estimatedCreditsMicros = estimatedCreditsMicros
94
+ }
95
+
96
+ public var effectivePromptTokens: Int {
97
+ if let p = promptTokens, p > 0 { return p }
98
+ if let i = inputTokens, i > 0 { return i }
99
+ return 0
100
+ }
101
+
102
+ public var effectiveOutputTokens: Int {
103
+ if let c = completionTokens, c > 0 { return c }
104
+ if let o = outputTokens, o > 0 { return o }
105
+ return 0
106
+ }
107
+
108
+ public var effectiveCachedTokens: Int {
109
+ if let c = cachedPromptTokens, c > 0 { return c }
110
+ if let ci = cachedInputTokens, ci > 0 { return ci }
111
+ if let r = cacheReadInputTokens, r > 0 { return r }
112
+ return 0
113
+ }
114
+
115
+ public var effectiveReasoningTokens: Int {
116
+ return reasoningOutputTokens ?? 0
117
+ }
118
+ }
119
+
120
+ public struct ParticipantInfo: Codable, Identifiable {
121
+ public var id: String {
122
+ if let aid = agentId, !aid.isEmpty { return aid }
123
+ if let n = name, !n.isEmpty { return n }
124
+ return generatedId
125
+ }
126
+ private var generatedId: String = UUID().uuidString
127
+ public var agentId: String?
128
+ public var name: String?
129
+ public var agentType: String?
130
+ public var model: String?
131
+ public var reasoningEffort: String?
132
+ public var status: String?
133
+ public var conclusion: String?
134
+ public var startedAtMs: Double?
135
+ public var finishedAtMs: Double?
136
+ public var usage: TokenUsage?
137
+ public var usageDelta: TokenUsage?
138
+
139
+ enum CodingKeys: String, CodingKey {
140
+ case agentId = "agent_id"
141
+ case name
142
+ case agentType = "agent_type"
143
+ case model
144
+ case reasoningEffort = "reasoning_effort"
145
+ case status
146
+ case conclusion
147
+ case startedAtMs = "started_at_ms"
148
+ case finishedAtMs = "finished_at_ms"
149
+ case usage
150
+ case usageDelta = "usage_delta"
151
+ }
152
+
153
+ public init(
154
+ agentId: String? = nil,
155
+ name: String? = nil,
156
+ agentType: String? = nil,
157
+ model: String? = nil,
158
+ reasoningEffort: String? = nil,
159
+ status: String? = nil,
160
+ conclusion: String? = nil,
161
+ startedAtMs: Double? = nil,
162
+ finishedAtMs: Double? = nil,
163
+ usage: TokenUsage? = nil,
164
+ usageDelta: TokenUsage? = nil
165
+ ) {
166
+ self.agentId = agentId
167
+ self.name = name
168
+ self.agentType = agentType
169
+ self.model = model
170
+ self.reasoningEffort = reasoningEffort
171
+ self.status = status
172
+ self.conclusion = conclusion
173
+ self.startedAtMs = startedAtMs
174
+ self.finishedAtMs = finishedAtMs
175
+ self.usage = usage
176
+ self.usageDelta = usageDelta
177
+ }
178
+
179
+ public var effectiveUsage: TokenUsage? {
180
+ return usageDelta ?? usage
181
+ }
182
+
183
+ public var displayModel: String {
184
+ return model ?? "unknown"
185
+ }
186
+
187
+ public var displayEffort: String? {
188
+ guard let effort = reasoningEffort, !effort.isEmpty, effort.lowercased() != "none" else {
189
+ return nil
190
+ }
191
+ return effort
192
+ }
193
+ }
194
+
195
+ public struct QuotaWindow: Codable, Identifiable {
196
+ public var id: String { "\(slot ?? "primary")_\(windowDurationMins ?? 0)" }
197
+ public var slot: String?
198
+ public var usedPercent: Double?
199
+ public var windowDurationMins: Int?
200
+ public var resetsAt: Double?
201
+ public var deltaPercentagePoints: Double?
202
+
203
+ enum CodingKeys: String, CodingKey {
204
+ case slot
205
+ case usedPercent = "used_percent"
206
+ case windowDurationMins = "window_duration_mins"
207
+ case resetsAt = "resets_at"
208
+ case deltaPercentagePoints = "delta_percentage_points"
209
+ }
210
+
211
+ public init(
212
+ slot: String? = nil,
213
+ usedPercent: Double? = nil,
214
+ windowDurationMins: Int? = nil,
215
+ resetsAt: Double? = nil,
216
+ deltaPercentagePoints: Double? = nil
217
+ ) {
218
+ self.slot = slot
219
+ self.usedPercent = usedPercent
220
+ self.windowDurationMins = windowDurationMins
221
+ self.resetsAt = resetsAt
222
+ self.deltaPercentagePoints = deltaPercentagePoints
223
+ }
224
+
225
+ public var label: String {
226
+ guard let mins = windowDurationMins else {
227
+ return slot?.capitalized ?? "Window"
228
+ }
229
+ if mins < 60 {
230
+ return "\(mins)m"
231
+ } else if mins < 1440 {
232
+ let hours = mins / 60
233
+ return "\(hours)h"
234
+ } else {
235
+ let days = mins / 1440
236
+ return "\(days)d"
237
+ }
238
+ }
239
+
240
+ public var remainingPercent: Double {
241
+ guard let used = usedPercent else { return 100.0 }
242
+ return max(0.0, 100.0 - used)
243
+ }
244
+
245
+ public var formattedResetsAt: String? {
246
+ guard let r = resetsAt, r > 0 else { return nil }
247
+ let date = Date(timeIntervalSince1970: r > 1_000_000_000_000 ? r / 1000.0 : r)
248
+ let now = Date()
249
+ let interval = date.timeIntervalSince(now)
250
+ if interval <= 0 {
251
+ return "resets now"
252
+ } else if interval < 86400 {
253
+ let mins = Int(interval) / 60
254
+ if mins < 60 {
255
+ return "resets in \(max(1, mins))m"
256
+ } else {
257
+ let hours = mins / 60
258
+ let remMins = mins % 60
259
+ return "resets in \(hours)h \(remMins)m"
260
+ }
261
+ }
262
+ let formatter = DateFormatter()
263
+ formatter.dateFormat = "HH:mm"
264
+ return "resets at \(formatter.string(from: date))"
265
+ }
266
+ }
267
+
268
+ public struct QuotaAllocationInfo: Codable {
269
+ public let allocationId: String?
270
+ public let allocatedPp: Double
271
+ public let tokenWeight: Int?
272
+ public let attributionMethod: String?
273
+ public let status: String?
274
+
275
+ enum CodingKeys: String, CodingKey {
276
+ case allocationId = "allocation_id"
277
+ case allocatedPp = "allocated_pp"
278
+ case tokenWeight = "token_weight"
279
+ case attributionMethod = "attribution_method"
280
+ case status
281
+ }
282
+
283
+ public init(
284
+ allocationId: String? = nil,
285
+ allocatedPp: Double,
286
+ tokenWeight: Int? = nil,
287
+ attributionMethod: String? = nil,
288
+ status: String? = nil
289
+ ) {
290
+ self.allocationId = allocationId
291
+ self.allocatedPp = allocatedPp
292
+ self.tokenWeight = tokenWeight
293
+ self.attributionMethod = attributionMethod
294
+ self.status = status
295
+ }
296
+ }
297
+
298
+ public struct DailyQuotaItem: Codable, Identifiable {
299
+ public var id: String {
300
+ let acc = accountId ?? ""
301
+ let b = bucketId ?? ""
302
+ return "\(acc)_\(b)_\(date)"
303
+ }
304
+ public let accountId: String?
305
+ public let bucketId: String?
306
+ public let date: String
307
+ public let observedDeltaPp: Double
308
+ public let unattributedPp: Double?
309
+ public let crossMidnightPendingPp: Double?
310
+ public let coverageStatus: String?
311
+
312
+ enum CodingKeys: String, CodingKey {
313
+ case accountId = "account_id"
314
+ case bucketId = "bucket_id"
315
+ case date
316
+ case observedDeltaPp = "observed_delta_pp"
317
+ case unattributedPp = "unattributed_pp"
318
+ case crossMidnightPendingPp = "cross_midnight_pending_pp"
319
+ case coverageStatus = "coverage_status"
320
+ }
321
+ }
322
+
323
+ public struct SessionAllocationItem: Codable {
324
+ public let allocatedPp: Double
325
+ public let attributionStatus: String
326
+ public let method: String?
327
+
328
+ enum CodingKeys: String, CodingKey {
329
+ case allocatedPp = "allocated_pp"
330
+ case attributionStatus = "attribution_status"
331
+ case method
332
+ }
333
+ }
334
+
335
+ public struct QuotaSummary: Codable {
336
+ public let revision: Int
337
+ public let exportedAtMs: Double?
338
+ public let timezone: String?
339
+ public let activeAccountId: String?
340
+ public let dailyUsage: [DailyQuotaItem]?
341
+ public let sessionAllocations: [String: SessionAllocationItem]?
342
+
343
+ enum CodingKeys: String, CodingKey {
344
+ case revision
345
+ case exportedAtMs = "exported_at_ms"
346
+ case timezone
347
+ case activeAccountId = "active_account_id"
348
+ case dailyUsage = "daily_usage"
349
+ case sessionAllocations = "session_allocations"
350
+ }
351
+ }
352
+
353
+ public struct DailyQuotaUsage: Identifiable {
354
+ public var id: String { dateString }
355
+ public let date: Date
356
+ public let dateString: String
357
+ public let displayDate: String
358
+ public let quotaDelta: Double?
359
+ public let tokens: Int
360
+ public let runs: Int
361
+ public let crossMidnightPendingPp: Double?
362
+ public let coverageStatus: String?
363
+
364
+ public init(
365
+ date: Date,
366
+ dateString: String,
367
+ displayDate: String,
368
+ quotaDelta: Double?,
369
+ tokens: Int,
370
+ runs: Int,
371
+ crossMidnightPendingPp: Double? = nil,
372
+ coverageStatus: String? = nil
373
+ ) {
374
+ self.date = date
375
+ self.dateString = dateString
376
+ self.displayDate = displayDate
377
+ self.quotaDelta = quotaDelta
378
+ self.tokens = tokens
379
+ self.runs = runs
380
+ self.crossMidnightPendingPp = crossMidnightPendingPp
381
+ self.coverageStatus = coverageStatus
382
+ }
383
+ }
384
+
385
+ public struct SkillUsage: Codable, Identifiable {
386
+ public var id: String { name }
387
+ public var name: String
388
+ public var count: Int?
389
+
390
+ public init(name: String, count: Int? = 1) {
391
+ self.name = name
392
+ self.count = count
393
+ }
394
+ }
395
+
396
+ public struct ToolCallInfo: Codable, Identifiable {
397
+ public var id: String { name }
398
+ public var name: String
399
+ public var count: Int?
400
+ public var isMcp: Bool?
401
+ public var category: String?
402
+
403
+ enum CodingKeys: String, CodingKey {
404
+ case name
405
+ case count
406
+ case isMcp = "is_mcp"
407
+ case category
408
+ }
409
+
410
+ public init(name: String, count: Int? = 1, isMcp: Bool? = false, category: String? = nil) {
411
+ self.name = name
412
+ self.count = count
413
+ self.isMcp = isMcp
414
+ self.category = category
415
+ }
416
+
417
+ public var displayName: String {
418
+ if isMcp == true {
419
+ return name.replacingOccurrences(of: "mcp__", with: "")
420
+ }
421
+ return name
422
+ }
423
+ }
424
+
425
+ public struct TrajectoryStep: Codable, Identifiable {
426
+ public var id: String { callId ?? "\(type ?? "step")_\(timestamp ?? "")_\(title ?? "")" }
427
+ public var type: String?
428
+ public var name: String?
429
+ public var title: String?
430
+ public var detail: String?
431
+ public var status: String?
432
+ public var isMcp: Bool?
433
+ public var callId: String?
434
+ public var timestamp: String?
435
+ public var durationMs: Int?
436
+
437
+ enum CodingKeys: String, CodingKey {
438
+ case type
439
+ case name
440
+ case title
441
+ case detail
442
+ case status
443
+ case isMcp = "is_mcp"
444
+ case callId = "call_id"
445
+ case timestamp
446
+ case durationMs = "duration_ms"
447
+ }
448
+
449
+ public init(
450
+ type: String? = nil,
451
+ name: String? = nil,
452
+ title: String? = nil,
453
+ detail: String? = nil,
454
+ status: String? = nil,
455
+ isMcp: Bool? = false,
456
+ callId: String? = nil,
457
+ timestamp: String? = nil,
458
+ durationMs: Int? = nil
459
+ ) {
460
+ self.type = type
461
+ self.name = name
462
+ self.title = title
463
+ self.detail = detail
464
+ self.status = status
465
+ self.isMcp = isMcp
466
+ self.callId = callId
467
+ self.timestamp = timestamp
468
+ self.durationMs = durationMs
469
+ }
470
+ }
471
+
472
+ public struct TaskLogEntry: Codable, Identifiable {
473
+ public var id: String { "\(timestamp ?? "")_\(type ?? "")_\(message ?? "")" }
474
+ public var timestamp: String?
475
+ public var level: String?
476
+ public var type: String?
477
+ public var message: String?
478
+
479
+ public init(timestamp: String? = nil, level: String? = nil, type: String? = nil, message: String? = nil) {
480
+ self.timestamp = timestamp
481
+ self.level = level
482
+ self.type = type
483
+ self.message = message
484
+ }
485
+ }
486
+
487
+ public struct TaskSummaryInfo: Codable {
488
+ public var goal: String?
489
+ public var conclusion: String?
490
+
491
+ public init(goal: String? = nil, conclusion: String? = nil) {
492
+ self.goal = goal
493
+ self.conclusion = conclusion
494
+ }
495
+ }
496
+
497
+ public struct TaskRun: Codable, Identifiable {
498
+ public var id: String {
499
+ if let s = sessionId, let t = turnId {
500
+ return "\(s)--\(t)"
501
+ }
502
+ return fileStem ?? UUID().uuidString
503
+ }
504
+
505
+ public var fileStem: String?
506
+ public var sessionId: String?
507
+ public var turnId: String?
508
+ public var startedAtMs: Double?
509
+ public var finishedAtMs: Double?
510
+ public var status: String?
511
+ public var thread: ThreadInfo?
512
+ public var cwd: String?
513
+ public var summary: String?
514
+ public var parent: ParticipantInfo?
515
+ public var workersDict: [String: ParticipantInfo]?
516
+ public var workersList: [ParticipantInfo]?
517
+ public var quotaBefore: [QuotaWindow]?
518
+ public var quotaAfter: [QuotaWindow]?
519
+ public var quotaChangeDuringRun: [QuotaWindow]?
520
+ public var skillsUsed: [SkillUsage]?
521
+ public var toolsUsed: [ToolCallInfo]?
522
+ public var trajectory: [TrajectoryStep]?
523
+ public var logs: [TaskLogEntry]?
524
+ public var summaryInfo: TaskSummaryInfo?
525
+ public var transcriptPath: String?
526
+ public var mergedInto: String?
527
+ public var isSystemTask: Bool?
528
+ public var allocatedQuotaDelta: Double?
529
+ public var quotaAllocation: QuotaAllocationInfo?
530
+
531
+ enum CodingKeys: String, CodingKey {
532
+ case sessionId = "session_id"
533
+ case turnId = "turn_id"
534
+ case startedAtMs = "started_at_ms"
535
+ case finishedAtMs = "finished_at_ms"
536
+ case status
537
+ case thread
538
+ case cwd
539
+ case summary
540
+ case parent
541
+ case workers
542
+ case quotaBefore = "quota_before"
543
+ case quotaAfter = "quota_after"
544
+ case quotaChangeDuringRun = "quota_change_during_run"
545
+ case skillsUsed = "skills_used"
546
+ case toolsUsed = "tools_used"
547
+ case trajectory
548
+ case logs
549
+ case summaryInfo = "summary_info"
550
+ case transcriptPath = "transcript_path"
551
+ case mergedInto = "merged_into"
552
+ case isSystemTask = "is_system_task"
553
+ case allocatedQuotaDelta = "allocated_quota_pp"
554
+ case quotaAllocation = "quota_allocation"
555
+ }
556
+
557
+ public init(from decoder: Decoder) throws {
558
+ let container = try decoder.container(keyedBy: CodingKeys.self)
559
+ sessionId = try container.decodeIfPresent(String.self, forKey: .sessionId)
560
+ turnId = try container.decodeIfPresent(String.self, forKey: .turnId)
561
+
562
+ if let s = try? container.decode(Double.self, forKey: .startedAtMs) {
563
+ startedAtMs = s
564
+ } else if let s = try? container.decode(Int.self, forKey: .startedAtMs) {
565
+ startedAtMs = Double(s)
566
+ }
567
+
568
+ if let f = try? container.decode(Double.self, forKey: .finishedAtMs) {
569
+ finishedAtMs = f
570
+ } else if let f = try? container.decode(Int.self, forKey: .finishedAtMs) {
571
+ finishedAtMs = Double(f)
572
+ }
573
+
574
+ status = try container.decodeIfPresent(String.self, forKey: .status)
575
+ thread = try container.decodeIfPresent(ThreadInfo.self, forKey: .thread)
576
+ cwd = try container.decodeIfPresent(String.self, forKey: .cwd)
577
+ summary = try container.decodeIfPresent(String.self, forKey: .summary)
578
+ parent = try container.decodeIfPresent(ParticipantInfo.self, forKey: .parent)
579
+ quotaBefore = try container.decodeIfPresent([QuotaWindow].self, forKey: .quotaBefore)
580
+ quotaAfter = try container.decodeIfPresent([QuotaWindow].self, forKey: .quotaAfter)
581
+ quotaChangeDuringRun = try container.decodeIfPresent([QuotaWindow].self, forKey: .quotaChangeDuringRun)
582
+ skillsUsed = try container.decodeIfPresent([SkillUsage].self, forKey: .skillsUsed)
583
+ toolsUsed = try container.decodeIfPresent([ToolCallInfo].self, forKey: .toolsUsed)
584
+ trajectory = try container.decodeIfPresent([TrajectoryStep].self, forKey: .trajectory)
585
+ logs = try container.decodeIfPresent([TaskLogEntry].self, forKey: .logs)
586
+ summaryInfo = try container.decodeIfPresent(TaskSummaryInfo.self, forKey: .summaryInfo)
587
+ transcriptPath = try container.decodeIfPresent(String.self, forKey: .transcriptPath)
588
+ mergedInto = try container.decodeIfPresent(String.self, forKey: .mergedInto)
589
+ isSystemTask = try container.decodeIfPresent(Bool.self, forKey: .isSystemTask)
590
+ allocatedQuotaDelta = try? container.decode(Double.self, forKey: .allocatedQuotaDelta)
591
+ quotaAllocation = try? container.decode(QuotaAllocationInfo.self, forKey: .quotaAllocation)
592
+ if allocatedQuotaDelta == nil, let alloc = quotaAllocation {
593
+ allocatedQuotaDelta = alloc.allocatedPp
594
+ }
595
+
596
+ if let dict = try? container.decode([String: ParticipantInfo].self, forKey: .workers) {
597
+ var updatedDict: [String: ParticipantInfo] = [:]
598
+ var list: [ParticipantInfo] = []
599
+ for (key, var worker) in dict {
600
+ if worker.agentId == nil || worker.agentId?.isEmpty == true {
601
+ worker.agentId = key
602
+ }
603
+ updatedDict[key] = worker
604
+ list.append(worker)
605
+ }
606
+ list.sort {
607
+ let s1 = $0.startedAtMs ?? 0
608
+ let s2 = $1.startedAtMs ?? 0
609
+ if s1 != s2 { return s1 < s2 }
610
+ return ($0.agentId ?? $0.id) < ($1.agentId ?? $1.id)
611
+ }
612
+ workersDict = updatedDict
613
+ workersList = list
614
+ } else if let list = try? container.decode([ParticipantInfo].self, forKey: .workers) {
615
+ workersList = list
616
+ }
617
+ }
618
+
619
+ public func encode(to encoder: Encoder) throws {
620
+ var container = encoder.container(keyedBy: CodingKeys.self)
621
+ try container.encodeIfPresent(sessionId, forKey: .sessionId)
622
+ try container.encodeIfPresent(turnId, forKey: .turnId)
623
+ try container.encodeIfPresent(startedAtMs, forKey: .startedAtMs)
624
+ try container.encodeIfPresent(finishedAtMs, forKey: .finishedAtMs)
625
+ try container.encodeIfPresent(status, forKey: .status)
626
+ try container.encodeIfPresent(thread, forKey: .thread)
627
+ try container.encodeIfPresent(cwd, forKey: .cwd)
628
+ try container.encodeIfPresent(summary, forKey: .summary)
629
+ try container.encodeIfPresent(parent, forKey: .parent)
630
+ if let d = workersDict {
631
+ try container.encode(d, forKey: .workers)
632
+ } else if let l = workersList {
633
+ try container.encode(l, forKey: .workers)
634
+ }
635
+ try container.encodeIfPresent(quotaBefore, forKey: .quotaBefore)
636
+ try container.encodeIfPresent(quotaAfter, forKey: .quotaAfter)
637
+ try container.encodeIfPresent(quotaChangeDuringRun, forKey: .quotaChangeDuringRun)
638
+ try container.encodeIfPresent(skillsUsed, forKey: .skillsUsed)
639
+ try container.encodeIfPresent(toolsUsed, forKey: .toolsUsed)
640
+ try container.encodeIfPresent(trajectory, forKey: .trajectory)
641
+ try container.encodeIfPresent(logs, forKey: .logs)
642
+ try container.encodeIfPresent(summaryInfo, forKey: .summaryInfo)
643
+ try container.encodeIfPresent(transcriptPath, forKey: .transcriptPath)
644
+ try container.encodeIfPresent(mergedInto, forKey: .mergedInto)
645
+ try container.encodeIfPresent(isSystemTask, forKey: .isSystemTask)
646
+ try container.encodeIfPresent(allocatedQuotaDelta, forKey: .allocatedQuotaDelta)
647
+ try container.encodeIfPresent(quotaAllocation, forKey: .quotaAllocation)
648
+ }
649
+
650
+ public init(
651
+ sessionId: String? = nil,
652
+ turnId: String? = nil,
653
+ startedAtMs: Double? = nil,
654
+ finishedAtMs: Double? = nil,
655
+ status: String? = nil,
656
+ thread: ThreadInfo? = nil,
657
+ cwd: String? = nil,
658
+ summary: String? = nil,
659
+ parent: ParticipantInfo? = nil,
660
+ workers: [ParticipantInfo]? = nil,
661
+ quotaBefore: [QuotaWindow]? = nil,
662
+ quotaAfter: [QuotaWindow]? = nil,
663
+ quotaChangeDuringRun: [QuotaWindow]? = nil,
664
+ skillsUsed: [SkillUsage]? = nil,
665
+ toolsUsed: [ToolCallInfo]? = nil,
666
+ trajectory: [TrajectoryStep]? = nil,
667
+ logs: [TaskLogEntry]? = nil,
668
+ summaryInfo: TaskSummaryInfo? = nil,
669
+ transcriptPath: String? = nil,
670
+ fileStem: String? = nil,
671
+ mergedInto: String? = nil,
672
+ isSystemTask: Bool? = nil,
673
+ allocatedQuotaDelta: Double? = nil,
674
+ quotaAllocation: QuotaAllocationInfo? = nil
675
+ ) {
676
+ self.sessionId = sessionId
677
+ self.turnId = turnId
678
+ self.startedAtMs = startedAtMs
679
+ self.finishedAtMs = finishedAtMs
680
+ self.status = status
681
+ self.thread = thread
682
+ self.cwd = cwd
683
+ self.summary = summary
684
+ self.parent = parent
685
+ self.workersList = workers
686
+ self.quotaBefore = quotaBefore
687
+ self.quotaAfter = quotaAfter
688
+ self.quotaChangeDuringRun = quotaChangeDuringRun
689
+ self.skillsUsed = skillsUsed
690
+ self.toolsUsed = toolsUsed
691
+ self.trajectory = trajectory
692
+ self.logs = logs
693
+ self.summaryInfo = summaryInfo
694
+ self.transcriptPath = transcriptPath
695
+ self.fileStem = fileStem
696
+ self.mergedInto = mergedInto
697
+ self.isSystemTask = isSystemTask
698
+ self.allocatedQuotaDelta = allocatedQuotaDelta ?? quotaAllocation?.allocatedPp
699
+ self.quotaAllocation = quotaAllocation
700
+ }
701
+
702
+ // MARK: - Computed Properties
703
+
704
+ public var projectName: String {
705
+ if isInternalTask && (thread?.cwd == "/" || cwd == "/" || cwd == nil) {
706
+ return L("System Task", "系统任务")
707
+ }
708
+ if let c = thread?.cwd ?? cwd, !c.isEmpty {
709
+ let last = URL(fileURLWithPath: c).lastPathComponent
710
+ if last.isEmpty || last == "/" {
711
+ return isInternalTask ? L("System Task", "系统任务") : "codex-flow"
712
+ }
713
+ return last
714
+ }
715
+ if let n = thread?.name, !n.isEmpty {
716
+ return n
717
+ }
718
+ return isInternalTask ? L("System Task", "系统任务") : "codex-flow"
719
+ }
720
+
721
+ public var gitBranch: String? {
722
+ return thread?.gitInfo?.branch
723
+ }
724
+
725
+ public var sessionTitle: String {
726
+ if let p = thread?.preview, !p.isEmpty { return p }
727
+ if let n = thread?.name, !n.isEmpty { return n }
728
+ if let s = summary, !s.isEmpty { return s }
729
+ if isInternalTask {
730
+ if let model = parent?.displayModel, !model.isEmpty {
731
+ return L("System Background Task (\(model))", "系统后台任务 (\(model))")
732
+ }
733
+ return L("System Background Task", "系统后台任务")
734
+ }
735
+ if let sid = sessionId, !sid.isEmpty {
736
+ return L("Task #\(String(sid.prefix(8)))", "任务 #\(String(sid.prefix(8)))")
737
+ }
738
+ return L("FlowPilot Task", "FlowPilot 任务")
739
+ }
740
+
741
+ public var effectiveGoal: String? {
742
+ if let g = summaryInfo?.goal, !g.isEmpty { return g }
743
+ if let p = thread?.preview, !p.isEmpty { return p }
744
+ if let s = summary, !s.isEmpty { return s }
745
+ if isInternalTask {
746
+ return sessionTitle
747
+ }
748
+ return nil
749
+ }
750
+
751
+ public var effectiveConclusion: String? {
752
+ return summaryInfo?.conclusion ?? summary
753
+ }
754
+
755
+ public var turnPreview: String {
756
+ if let goal = summaryInfo?.goal, !goal.isEmpty {
757
+ return TaskRun.cleanPromptText(goal)
758
+ }
759
+ if let sum = summary, !sum.isEmpty {
760
+ return sum
761
+ }
762
+ if let conclusion = summaryInfo?.conclusion, !conclusion.isEmpty {
763
+ return TaskRun.cleanPromptText(conclusion)
764
+ }
765
+ return thread?.preview ?? thread?.name ?? sessionTitle
766
+ }
767
+
768
+ public static func cleanPromptText(_ raw: String) -> String {
769
+ var text = raw.trimmingCharacters(in: .whitespacesAndNewlines)
770
+ if let reqRange = text.range(of: "## My request:", options: .caseInsensitive) {
771
+ text = String(text[reqRange.upperBound...]).trimmingCharacters(in: .whitespacesAndNewlines)
772
+ } else if let reqRange = text.range(of: "<USER_REQUEST>") {
773
+ if let endRange = text.range(of: "</USER_REQUEST>") {
774
+ text = String(text[reqRange.upperBound..<endRange.lowerBound]).trimmingCharacters(in: .whitespacesAndNewlines)
775
+ } else {
776
+ text = String(text[reqRange.upperBound...]).trimmingCharacters(in: .whitespacesAndNewlines)
777
+ }
778
+ }
779
+
780
+ if let regex = try? NSRegularExpression(pattern: "<image[^>]*>|<file[^>]*>", options: .caseInsensitive) {
781
+ let range = NSRange(text.startIndex..., in: text)
782
+ text = regex.stringByReplacingMatches(in: text, range: range, withTemplate: "")
783
+ }
784
+
785
+ let components = text.components(separatedBy: .whitespacesAndNewlines).filter { !$0.isEmpty }
786
+ let result = components.joined(separator: " ").trimmingCharacters(in: .whitespacesAndNewlines)
787
+ return result.isEmpty ? raw : result
788
+ }
789
+
790
+ public var hasSkillsOrTools: Bool {
791
+ return !(skillsUsed ?? []).isEmpty || !(toolsUsed ?? []).isEmpty
792
+ }
793
+
794
+ public var hasTrajectory: Bool {
795
+ return !(trajectory ?? []).isEmpty
796
+ }
797
+
798
+ public var hasLogs: Bool {
799
+ return !(logs ?? []).isEmpty
800
+ }
801
+
802
+ public var isAborted: Bool {
803
+ if let st = status?.lowercased() {
804
+ return st == "aborted" || st == "interrupted" || st == "cancelled"
805
+ }
806
+ if finishedAtMs == nil, let s = startedAtMs {
807
+ return (Date().timeIntervalSince1970 * 1000 - s) > 300_000
808
+ }
809
+ return false
810
+ }
811
+
812
+ public var isInternalTask: Bool {
813
+ return isSystemTask == true || cwd == "/"
814
+ }
815
+
816
+ public var isRunning: Bool {
817
+ if isAborted { return false }
818
+ if let st = status?.lowercased() {
819
+ return st == "running" || st == "in_progress" || st == "active"
820
+ }
821
+ return finishedAtMs == nil
822
+ }
823
+
824
+ public var isSuccess: Bool {
825
+ if isAborted { return false }
826
+ if let st = status?.lowercased() {
827
+ return st == "success" || st == "completed" || st == "finished"
828
+ }
829
+ return finishedAtMs != nil
830
+ }
831
+
832
+ public var isError: Bool {
833
+ if isAborted { return false }
834
+ if let st = status?.lowercased() {
835
+ return st == "error" || st == "failed"
836
+ }
837
+ return false
838
+ }
839
+
840
+ public var durationSeconds: Double {
841
+ guard let s = startedAtMs else { return 0 }
842
+ let e = finishedAtMs ?? (Date().timeIntervalSince1970 * 1000)
843
+ return max(0, (e - s) / 1000.0)
844
+ }
845
+
846
+ public var formattedDuration: String {
847
+ let d = durationSeconds
848
+ if d < 1.0 {
849
+ return String(format: "%.0f ms", d * 1000)
850
+ } else if d < 60.0 {
851
+ return String(format: "%.1f s", d)
852
+ } else {
853
+ let mins = Int(d) / 60
854
+ let secs = Int(d) % 60
855
+ return "\(mins)m \(secs)s"
856
+ }
857
+ }
858
+
859
+ public var formattedDate: String {
860
+ guard let s = startedAtMs else { return "--:--" }
861
+ let date = Date(timeIntervalSince1970: s / 1000.0)
862
+ let calendar = Calendar.current
863
+ let formatter = DateFormatter()
864
+ if calendar.isDateInToday(date) {
865
+ formatter.dateFormat = "HH:mm"
866
+ return "Today \(formatter.string(from: date))"
867
+ } else {
868
+ formatter.dateFormat = "MM-dd HH:mm"
869
+ return formatter.string(from: date)
870
+ }
871
+ }
872
+
873
+ public var allWorkers: [ParticipantInfo] {
874
+ if let list = workersList { return list }
875
+ guard let dict = workersDict else { return [] }
876
+ return Array(dict.values).sorted {
877
+ let s1 = $0.startedAtMs ?? 0
878
+ let s2 = $1.startedAtMs ?? 0
879
+ if s1 != s2 { return s1 < s2 }
880
+ return ($0.agentId ?? $0.id) < ($1.agentId ?? $1.id)
881
+ }
882
+ }
883
+
884
+ public var effectiveQuotaWindows: [QuotaWindow] {
885
+ if let current = quotaChangeDuringRun, !current.isEmpty {
886
+ if current.contains(where: { $0.deltaPercentagePoints == nil }),
887
+ let before = quotaBefore, !before.isEmpty {
888
+ return current.map { w in
889
+ var window = w
890
+ if window.deltaPercentagePoints == nil {
891
+ let match = before.first(where: {
892
+ ($0.windowDurationMins != nil && $0.windowDurationMins == w.windowDurationMins) ||
893
+ ($0.slot != nil && $0.slot == w.slot)
894
+ })
895
+ if let afterUsed = w.usedPercent, let beforeUsed = match?.usedPercent {
896
+ window.deltaPercentagePoints = afterUsed - beforeUsed
897
+ }
898
+ }
899
+ return window
900
+ }
901
+ }
902
+ return current
903
+ }
904
+ if let after = quotaAfter, !after.isEmpty {
905
+ if let before = quotaBefore, !before.isEmpty {
906
+ return after.map { w in
907
+ var window = w
908
+ if window.deltaPercentagePoints == nil {
909
+ let match = before.first(where: {
910
+ ($0.windowDurationMins != nil && $0.windowDurationMins == w.windowDurationMins) ||
911
+ ($0.slot != nil && $0.slot == w.slot)
912
+ })
913
+ if let afterUsed = w.usedPercent, let beforeUsed = match?.usedPercent {
914
+ window.deltaPercentagePoints = afterUsed - beforeUsed
915
+ }
916
+ }
917
+ return window
918
+ }
919
+ }
920
+ return after
921
+ }
922
+ return quotaBefore ?? []
923
+ }
924
+
925
+ public static let shortQuotaWindowMinutes = 300
926
+ public static let weeklyQuotaWindowMinutes = 10_080
927
+
928
+ private func quotaWindow(durationMinutes: Int) -> QuotaWindow? {
929
+ return effectiveQuotaWindows.first { $0.windowDurationMins == durationMinutes }
930
+ }
931
+
932
+ public var shortWindowQuotaDelta: Double? {
933
+ return quotaWindow(durationMinutes: Self.shortQuotaWindowMinutes)?.deltaPercentagePoints
934
+ }
935
+
936
+ public var weeklyQuotaDelta: Double? {
937
+ return quotaWindow(durationMinutes: Self.weeklyQuotaWindowMinutes)?.deltaPercentagePoints
938
+ }
939
+
940
+ /// Observed account watermark movement during execution.
941
+ /// Used ONLY for single-run detail inspection/evidence; strictly forbidden for cumulative statistics.
942
+ public var observedAccountDelta: Double? {
943
+ return weeklyQuotaDelta
944
+ }
945
+
946
+ /// Canonical quota movement used by task, project, model, chat and trend analytics.
947
+ /// Strictly returns allocatedQuotaDelta; returns nil if attribution is missing or pending.
948
+ /// NEVER falls back to observedAccountDelta or short window to prevent double counting.
949
+ public var canonicalQuotaDelta: Double? {
950
+ return allocatedQuotaDelta
951
+ }
952
+
953
+ /// Backward-compatible name used by existing analytics consumers.
954
+ public var primaryQuotaDelta: Double? {
955
+ return canonicalQuotaDelta
956
+ }
957
+
958
+ public var shortWindowQuotaRemaining: Double? {
959
+ return quotaWindow(durationMinutes: Self.shortQuotaWindowMinutes)?.remainingPercent
960
+ }
961
+
962
+ public var weeklyQuotaRemaining: Double? {
963
+ return quotaWindow(durationMinutes: Self.weeklyQuotaWindowMinutes)?.remainingPercent
964
+ }
965
+
966
+ public var primaryQuotaRemaining: Double? {
967
+ return shortWindowQuotaRemaining
968
+ }
969
+
970
+ public var aggregatedUsage: TokenUsage {
971
+ var total = 0
972
+ var prompt = 0
973
+ var completion = 0
974
+ var cached = 0
975
+ var reasoning = 0
976
+ var credits: Double = 0
977
+
978
+ let allParticipants = [parent].compactMap { $0 } + allWorkers
979
+ for p in allParticipants {
980
+ if let u = p.effectiveUsage {
981
+ total += u.totalTokens ?? (u.effectivePromptTokens + u.effectiveOutputTokens)
982
+ prompt += u.effectivePromptTokens
983
+ completion += u.effectiveOutputTokens
984
+ cached += u.effectiveCachedTokens
985
+ reasoning += u.effectiveReasoningTokens
986
+ credits += u.estimatedCreditsMicros ?? 0
987
+ }
988
+ }
989
+
990
+ return TokenUsage(
991
+ totalTokens: total > 0 ? total : nil,
992
+ promptTokens: prompt > 0 ? prompt : nil,
993
+ completionTokens: completion > 0 ? completion : nil,
994
+ cachedPromptTokens: cached > 0 ? cached : nil,
995
+ reasoningOutputTokens: reasoning > 0 ? reasoning : nil,
996
+ estimatedCreditsMicros: credits > 0 ? credits : nil
997
+ )
998
+ }
999
+
1000
+ public var totalTokens: Int {
1001
+ return aggregatedUsage.totalTokens ?? 0
1002
+ }
1003
+
1004
+ public var workerUsage: TokenUsage {
1005
+ var total = 0
1006
+ var prompt = 0
1007
+ var completion = 0
1008
+ var cached = 0
1009
+ var reasoning = 0
1010
+ var credits: Double = 0
1011
+
1012
+ for p in allWorkers {
1013
+ if let u = p.effectiveUsage {
1014
+ total += u.totalTokens ?? (u.effectivePromptTokens + u.effectiveOutputTokens)
1015
+ prompt += u.effectivePromptTokens
1016
+ completion += u.effectiveOutputTokens
1017
+ cached += u.effectiveCachedTokens
1018
+ reasoning += u.effectiveReasoningTokens
1019
+ credits += u.estimatedCreditsMicros ?? 0
1020
+ }
1021
+ }
1022
+
1023
+ return TokenUsage(
1024
+ totalTokens: total > 0 ? total : nil,
1025
+ promptTokens: prompt > 0 ? prompt : nil,
1026
+ completionTokens: completion > 0 ? completion : nil,
1027
+ cachedPromptTokens: cached > 0 ? cached : nil,
1028
+ reasoningOutputTokens: reasoning > 0 ? reasoning : nil,
1029
+ estimatedCreditsMicros: credits > 0 ? credits : nil
1030
+ )
1031
+ }
1032
+
1033
+ public var workerTotalTokens: Int {
1034
+ return workerUsage.totalTokens ?? 0
1035
+ }
1036
+
1037
+ public var parentTotalTokens: Int {
1038
+ guard let u = parent?.effectiveUsage else { return 0 }
1039
+ return u.totalTokens ?? (u.effectivePromptTokens + u.effectiveOutputTokens)
1040
+ }
1041
+
1042
+ public var formattedWorkerTokens: String {
1043
+ return TaskRun.formatTokenCount(workerTotalTokens)
1044
+ }
1045
+
1046
+ public var workerTokenShare: Double {
1047
+ let total = totalTokens
1048
+ guard total > 0 else { return 0.0 }
1049
+ return Double(workerTotalTokens) / Double(total)
1050
+ }
1051
+
1052
+ public var formattedWorkerSharePercent: String {
1053
+ return String(format: "%.1f%%", workerTokenShare * 100.0)
1054
+ }
1055
+
1056
+ public var formattedTotalTokens: String {
1057
+ let count = totalTokens
1058
+ return TaskRun.formatTokenCount(count)
1059
+ }
1060
+
1061
+ public var formattedCompactTokens: String {
1062
+ let count = aggregatedUsage.totalTokens ?? 0
1063
+ return TaskRun.formatCompactTokenCount(count)
1064
+ }
1065
+
1066
+ public var formattedCost: String {
1067
+ if let creditsMicros = aggregatedUsage.estimatedCreditsMicros, creditsMicros > 0 {
1068
+ let dollars = creditsMicros / 1_000_000.0
1069
+ return String(format: "$%.3f", dollars)
1070
+ }
1071
+ return "--"
1072
+ }
1073
+
1074
+ public static func formatTokenCount(_ count: Int) -> String {
1075
+ if count >= 1_000_000 {
1076
+ return String(format: "%.1fM", Double(count) / 1_000_000.0)
1077
+ } else if count >= 1_000 {
1078
+ return String(format: "%.1fk", Double(count) / 1_000.0)
1079
+ } else {
1080
+ return "\(count)"
1081
+ }
1082
+ }
1083
+
1084
+ public static func formatCompactTokenCount(_ count: Int) -> String {
1085
+ if count >= 1_000_000 {
1086
+ return String(format: "%.0fM", round(Double(count) / 1_000_000.0))
1087
+ } else if count >= 1_000 {
1088
+ return String(format: "%.0fk", round(Double(count) / 1_000.0))
1089
+ } else {
1090
+ return "\(count)"
1091
+ }
1092
+ }
1093
+
1094
+ // Preview sample data for testing and mock rendering
1095
+ public static var previewSample: TaskRun {
1096
+ return TaskRun(
1097
+ sessionId: "sess-84920",
1098
+ turnId: "turn-01",
1099
+ startedAtMs: Date().addingTimeInterval(-14.2).timeIntervalSince1970 * 1000,
1100
+ finishedAtMs: Date().timeIntervalSince1970 * 1000,
1101
+ status: "completed",
1102
+ thread: ThreadInfo(
1103
+ name: "codex-flow",
1104
+ preview: "feat: macOS native floating widget with hover expand",
1105
+ cwd: "/Users/parsifal/Repo/SkillHub/codex-flow",
1106
+ gitInfo: GitInfo(branch: "main")
1107
+ ),
1108
+ cwd: "/Users/parsifal/Repo/SkillHub/codex-flow",
1109
+ summary: "成功实现 macOS 原生悬浮窗与 Summary UI,支持光标悬停平滑展开、动态环形指标、Quota 配额监控与多 Agent 拓扑展示。",
1110
+ parent: ParticipantInfo(
1111
+ name: "Parent Agent",
1112
+ agentType: "orchestrator",
1113
+ model: "gpt-5-pro",
1114
+ reasoningEffort: "high",
1115
+ status: "completed",
1116
+ usage: TokenUsage(
1117
+ totalTokens: 14200,
1118
+ promptTokens: 8500,
1119
+ completionTokens: 5700,
1120
+ cachedPromptTokens: 3200,
1121
+ reasoningOutputTokens: 2100,
1122
+ estimatedCreditsMicros: 18000
1123
+ )
1124
+ ),
1125
+ workers: [
1126
+ ParticipantInfo(
1127
+ name: "Worker 1 (UI Engine)",
1128
+ agentType: "subagent",
1129
+ model: "gpt-5-flash",
1130
+ reasoningEffort: "medium",
1131
+ status: "completed",
1132
+ usage: TokenUsage(
1133
+ totalTokens: 26100,
1134
+ promptTokens: 14000,
1135
+ completionTokens: 12100,
1136
+ cachedPromptTokens: 8200,
1137
+ reasoningOutputTokens: 3800,
1138
+ estimatedCreditsMicros: 14000
1139
+ )
1140
+ ),
1141
+ ParticipantInfo(
1142
+ name: "Worker 2 (IPC & Telemetry)",
1143
+ agentType: "subagent",
1144
+ model: "gpt-5-flash",
1145
+ reasoningEffort: "medium",
1146
+ status: "completed",
1147
+ usage: TokenUsage(
1148
+ totalTokens: 18100,
1149
+ promptTokens: 9800,
1150
+ completionTokens: 8300,
1151
+ cachedPromptTokens: 4500,
1152
+ reasoningOutputTokens: 2400,
1153
+ estimatedCreditsMicros: 10000
1154
+ )
1155
+ )
1156
+ ],
1157
+ quotaBefore: [
1158
+ QuotaWindow(slot: "primary", usedPercent: 12.0, windowDurationMins: 300, resetsAt: Date().addingTimeInterval(180).timeIntervalSince1970),
1159
+ QuotaWindow(slot: "secondary", usedPercent: 34.0, windowDurationMins: 10_080, resetsAt: Date().addingTimeInterval(2400).timeIntervalSince1970)
1160
+ ],
1161
+ quotaChangeDuringRun: [
1162
+ QuotaWindow(slot: "primary", usedPercent: 18.0, windowDurationMins: 300, resetsAt: Date().addingTimeInterval(180).timeIntervalSince1970, deltaPercentagePoints: 6.0),
1163
+ QuotaWindow(slot: "secondary", usedPercent: 39.0, windowDurationMins: 10_080, resetsAt: Date().addingTimeInterval(2400).timeIntervalSince1970, deltaPercentagePoints: 5.0)
1164
+ ]
1165
+ )
1166
+ }
1167
+ }
1168
+
1169
+ // MARK: - Chat Session Aggregate (Dimension 2: Chat)
1170
+ public struct ChatSession: Identifiable {
1171
+ public var id: String { sessionId }
1172
+ public var sessionId: String
1173
+ public var projectName: String
1174
+ public var gitBranch: String?
1175
+ public var cwd: String?
1176
+ public var title: String
1177
+ public var summary: String?
1178
+ public var startedAtMs: Double?
1179
+ public var finishedAtMs: Double?
1180
+ public var lastActiveAtMs: Double
1181
+ public var runs: [TaskRun] // all session runs in this chat, sorted newest first
1182
+
1183
+ public init(
1184
+ sessionId: String,
1185
+ projectName: String,
1186
+ gitBranch: String? = nil,
1187
+ cwd: String? = nil,
1188
+ title: String,
1189
+ summary: String? = nil,
1190
+ startedAtMs: Double? = nil,
1191
+ finishedAtMs: Double? = nil,
1192
+ lastActiveAtMs: Double? = nil,
1193
+ runs: [TaskRun] = []
1194
+ ) {
1195
+ self.sessionId = sessionId
1196
+ self.projectName = projectName
1197
+ self.gitBranch = gitBranch
1198
+ self.cwd = cwd
1199
+ self.title = title
1200
+ self.summary = summary
1201
+ self.startedAtMs = startedAtMs
1202
+ self.finishedAtMs = finishedAtMs
1203
+ self.lastActiveAtMs = lastActiveAtMs ?? finishedAtMs ?? startedAtMs ?? 0
1204
+ self.runs = runs
1205
+ }
1206
+
1207
+ // MARK: - Aggregated Metrics
1208
+
1209
+ public var totalRunsCount: Int {
1210
+ return runs.count
1211
+ }
1212
+
1213
+ public var isAborted: Bool {
1214
+ guard !runs.isEmpty else { return false }
1215
+ if totalTokens == 0 && runs.contains(where: { $0.isAborted }) {
1216
+ return true
1217
+ }
1218
+ return runs.allSatisfy { $0.isAborted }
1219
+ }
1220
+
1221
+ public var isInternalTask: Bool {
1222
+ if cwd == "/" { return true }
1223
+ guard !runs.isEmpty else { return false }
1224
+ return runs.allSatisfy { $0.isInternalTask }
1225
+ }
1226
+
1227
+ public var isRunning: Bool {
1228
+ return runs.contains { $0.isRunning }
1229
+ }
1230
+
1231
+ public var isError: Bool {
1232
+ return runs.contains { $0.isError }
1233
+ }
1234
+
1235
+ public var isSuccess: Bool {
1236
+ return !isRunning && !isError && !isAborted && !runs.isEmpty
1237
+ }
1238
+
1239
+ public var totalDurationSeconds: Double {
1240
+ return runs.reduce(0.0) { $0 + $1.durationSeconds }
1241
+ }
1242
+
1243
+ public var formattedDuration: String {
1244
+ let d = totalDurationSeconds
1245
+ if d < 1.0 {
1246
+ return String(format: "%.0f ms", d * 1000)
1247
+ } else if d < 60.0 {
1248
+ return String(format: "%.1f s", d)
1249
+ } else {
1250
+ let mins = Int(d) / 60
1251
+ let secs = Int(d) % 60
1252
+ return "\(mins)m \(secs)s"
1253
+ }
1254
+ }
1255
+
1256
+ public var aggregatedUsage: TokenUsage {
1257
+ var total = 0
1258
+ var prompt = 0
1259
+ var completion = 0
1260
+ var cached = 0
1261
+ var reasoning = 0
1262
+ var credits: Double = 0
1263
+
1264
+ for run in runs {
1265
+ let u = run.aggregatedUsage
1266
+ total += u.totalTokens ?? 0
1267
+ prompt += u.effectivePromptTokens
1268
+ completion += u.effectiveOutputTokens
1269
+ cached += u.effectiveCachedTokens
1270
+ reasoning += u.effectiveReasoningTokens
1271
+ credits += u.estimatedCreditsMicros ?? 0
1272
+ }
1273
+
1274
+ return TokenUsage(
1275
+ totalTokens: total > 0 ? total : nil,
1276
+ promptTokens: prompt > 0 ? prompt : nil,
1277
+ completionTokens: completion > 0 ? completion : nil,
1278
+ cachedPromptTokens: cached > 0 ? cached : nil,
1279
+ reasoningOutputTokens: reasoning > 0 ? reasoning : nil,
1280
+ estimatedCreditsMicros: credits > 0 ? credits : nil
1281
+ )
1282
+ }
1283
+
1284
+ public var totalTokens: Int {
1285
+ return aggregatedUsage.totalTokens ?? 0
1286
+ }
1287
+
1288
+ public var workerUsage: TokenUsage {
1289
+ var total = 0
1290
+ var prompt = 0
1291
+ var completion = 0
1292
+ var cached = 0
1293
+ var reasoning = 0
1294
+ var credits: Double = 0
1295
+
1296
+ for run in runs {
1297
+ let u = run.workerUsage
1298
+ total += u.totalTokens ?? 0
1299
+ prompt += u.effectivePromptTokens
1300
+ completion += u.effectiveOutputTokens
1301
+ cached += u.effectiveCachedTokens
1302
+ reasoning += u.effectiveReasoningTokens
1303
+ credits += u.estimatedCreditsMicros ?? 0
1304
+ }
1305
+
1306
+ return TokenUsage(
1307
+ totalTokens: total > 0 ? total : nil,
1308
+ promptTokens: prompt > 0 ? prompt : nil,
1309
+ completionTokens: completion > 0 ? completion : nil,
1310
+ cachedPromptTokens: cached > 0 ? cached : nil,
1311
+ reasoningOutputTokens: reasoning > 0 ? reasoning : nil,
1312
+ estimatedCreditsMicros: credits > 0 ? credits : nil
1313
+ )
1314
+ }
1315
+
1316
+ public var workerTotalTokens: Int {
1317
+ return workerUsage.totalTokens ?? 0
1318
+ }
1319
+
1320
+ public var parentTotalTokens: Int {
1321
+ return runs.reduce(0) { $0 + $1.parentTotalTokens }
1322
+ }
1323
+
1324
+ public var formattedWorkerTokens: String {
1325
+ return TaskRun.formatTokenCount(workerTotalTokens)
1326
+ }
1327
+
1328
+ public var workerTokenShare: Double {
1329
+ let total = totalTokens
1330
+ guard total > 0 else { return 0.0 }
1331
+ return Double(workerTotalTokens) / Double(total)
1332
+ }
1333
+
1334
+ public var formattedWorkerSharePercent: String {
1335
+ return String(format: "%.1f%%", workerTokenShare * 100.0)
1336
+ }
1337
+
1338
+ public var formattedTotalTokens: String {
1339
+ return TaskRun.formatTokenCount(totalTokens)
1340
+ }
1341
+
1342
+ public var formattedCost: String {
1343
+ if let creditsMicros = aggregatedUsage.estimatedCreditsMicros, creditsMicros > 0 {
1344
+ let dollars = creditsMicros / 1_000_000.0
1345
+ return String(format: "$%.3f", dollars)
1346
+ }
1347
+ return "--"
1348
+ }
1349
+
1350
+ public var maxWorkerCount: Int {
1351
+ return runs.map { $0.allWorkers.count }.max() ?? 0
1352
+ }
1353
+
1354
+ public var totalQuotaDelta: Double? {
1355
+ var total: Double = 0
1356
+ var hasDelta = false
1357
+ for r in runs {
1358
+ if let d = r.canonicalQuotaDelta {
1359
+ total += d
1360
+ hasDelta = true
1361
+ }
1362
+ }
1363
+ return hasDelta ? total : nil
1364
+ }
1365
+
1366
+ public var isPartiallyAttributed: Bool {
1367
+ let allocatedCount = runs.filter { $0.canonicalQuotaDelta != nil }.count
1368
+ return allocatedCount > 0 && allocatedCount < runs.count
1369
+ }
1370
+
1371
+ public var isFullyAttributed: Bool {
1372
+ guard !runs.isEmpty else { return false }
1373
+ return runs.allSatisfy { $0.canonicalQuotaDelta != nil }
1374
+ }
1375
+
1376
+ public var observedAccountDelta: Double? {
1377
+ var total: Double = 0
1378
+ var hasObserved = false
1379
+ for r in runs {
1380
+ if let d = r.observedAccountDelta {
1381
+ total += d
1382
+ hasObserved = true
1383
+ }
1384
+ }
1385
+ return hasObserved ? total : nil
1386
+ }
1387
+
1388
+ public var totalWorkersCount: Int {
1389
+ return runs.reduce(0) { $0 + $1.allWorkers.count }
1390
+ }
1391
+
1392
+ public var latestRun: TaskRun? {
1393
+ return runs.first
1394
+ }
1395
+
1396
+ public var localizedFormattedDate: String {
1397
+ guard lastActiveAtMs > 0 else { return "--:--" }
1398
+ let date = Date(timeIntervalSince1970: lastActiveAtMs / 1000.0)
1399
+ let calendar = Calendar.current
1400
+ let formatter = DateFormatter()
1401
+ if calendar.isDateInToday(date) {
1402
+ formatter.dateFormat = "HH:mm"
1403
+ return L("Today \(formatter.string(from: date))", "今天 \(formatter.string(from: date))")
1404
+ } else {
1405
+ formatter.dateFormat = "MM-dd HH:mm"
1406
+ return formatter.string(from: date)
1407
+ }
1408
+ }
1409
+ }
1410
+
1411
+ // MARK: - Analytics & Stats Data Models
1412
+
1413
+ public struct ModelStats: Identifiable {
1414
+ public var id: String { name }
1415
+ public var name: String
1416
+ public var calls: Int
1417
+ public var tokens: Int
1418
+ public var roles: [String]
1419
+ public var quotaDelta: Double?
1420
+ public var estimatedCost: Double?
1421
+
1422
+ public init(
1423
+ name: String,
1424
+ calls: Int,
1425
+ tokens: Int,
1426
+ roles: [String],
1427
+ quotaDelta: Double? = nil,
1428
+ estimatedCost: Double? = nil
1429
+ ) {
1430
+ self.name = name
1431
+ self.calls = calls
1432
+ self.tokens = tokens
1433
+ self.roles = roles
1434
+ self.quotaDelta = quotaDelta
1435
+ self.estimatedCost = estimatedCost
1436
+ }
1437
+ }
1438
+
1439
+ public struct ProjectStats: Identifiable {
1440
+ public var id: String { name }
1441
+ public var name: String
1442
+ public var runs: Int
1443
+ public var delegated: Int
1444
+ public var tokens: Int
1445
+ public var durationMs: Double
1446
+ public var quotaDelta: Double?
1447
+ public var estimatedCost: Double?
1448
+
1449
+ public init(
1450
+ name: String,
1451
+ runs: Int,
1452
+ delegated: Int,
1453
+ tokens: Int,
1454
+ durationMs: Double,
1455
+ quotaDelta: Double? = nil,
1456
+ estimatedCost: Double? = nil
1457
+ ) {
1458
+ self.name = name
1459
+ self.runs = runs
1460
+ self.delegated = delegated
1461
+ self.tokens = tokens
1462
+ self.durationMs = durationMs
1463
+ self.quotaDelta = quotaDelta
1464
+ self.estimatedCost = estimatedCost
1465
+ }
1466
+
1467
+ public var formattedDuration: String {
1468
+ let secs = durationMs / 1000.0
1469
+ if secs < 60 {
1470
+ return String(format: "%.0fs", secs)
1471
+ } else if secs < 3600 {
1472
+ return String(format: "%.1fm", secs / 60.0)
1473
+ } else {
1474
+ return String(format: "%.1fh", secs / 3600.0)
1475
+ }
1476
+ }
1477
+ }
1478
+
1479
+ public struct TelemetryStats {
1480
+ public var days: Int
1481
+ public var projectFilter: String?
1482
+ public var totalRuns: Int
1483
+ public var delegatedRuns: Int
1484
+ public var directRuns: Int
1485
+ public var totalDurationMs: Double
1486
+ public var totalTokens: Int
1487
+ public var parentTokens: Int
1488
+ public var workerTokens: Int
1489
+ public var cachedInputTokens: Int
1490
+ public var inputTokens: Int
1491
+ public var outputTokens: Int
1492
+ public var reasoningOutputTokens: Int
1493
+ public var cacheRatio: Double
1494
+ public var workerOffloadRatio: Double
1495
+ public var estimatedCreditsMicros: Double?
1496
+ public var models: [ModelStats]
1497
+ public var projects: [ProjectStats]
1498
+
1499
+ public init(
1500
+ days: Int = 30,
1501
+ projectFilter: String? = nil,
1502
+ totalRuns: Int = 0,
1503
+ delegatedRuns: Int = 0,
1504
+ directRuns: Int = 0,
1505
+ totalDurationMs: Double = 0,
1506
+ totalTokens: Int = 0,
1507
+ parentTokens: Int = 0,
1508
+ workerTokens: Int = 0,
1509
+ cachedInputTokens: Int = 0,
1510
+ inputTokens: Int = 0,
1511
+ outputTokens: Int = 0,
1512
+ reasoningOutputTokens: Int = 0,
1513
+ cacheRatio: Double = 0,
1514
+ workerOffloadRatio: Double = 0,
1515
+ estimatedCreditsMicros: Double? = nil,
1516
+ models: [ModelStats] = [],
1517
+ projects: [ProjectStats] = []
1518
+ ) {
1519
+ self.days = days
1520
+ self.projectFilter = projectFilter
1521
+ self.totalRuns = totalRuns
1522
+ self.delegatedRuns = delegatedRuns
1523
+ self.directRuns = directRuns
1524
+ self.totalDurationMs = totalDurationMs
1525
+ self.totalTokens = totalTokens
1526
+ self.parentTokens = parentTokens
1527
+ self.workerTokens = workerTokens
1528
+ self.cachedInputTokens = cachedInputTokens
1529
+ self.inputTokens = inputTokens
1530
+ self.outputTokens = outputTokens
1531
+ self.reasoningOutputTokens = reasoningOutputTokens
1532
+ self.cacheRatio = cacheRatio
1533
+ self.workerOffloadRatio = workerOffloadRatio
1534
+ self.estimatedCreditsMicros = estimatedCreditsMicros
1535
+ self.models = models
1536
+ self.projects = projects
1537
+ }
1538
+
1539
+ public var formattedTotalDuration: String {
1540
+ let secs = totalDurationMs / 1000.0
1541
+ if secs < 60 {
1542
+ return String(format: "%.0fs", secs)
1543
+ } else if secs < 3600 {
1544
+ return String(format: "%.1fm", secs / 60.0)
1545
+ } else {
1546
+ let hours = secs / 3600.0
1547
+ return String(format: "%.1fh", hours)
1548
+ }
1549
+ }
1550
+
1551
+ public var formattedCost: String {
1552
+ if let micros = estimatedCreditsMicros, micros > 0 {
1553
+ return String(format: "$%.3f", micros / 1_000_000.0)
1554
+ }
1555
+ return "--"
1556
+ }
1557
+ }