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.
- codex_flow/__init__.py +28 -0
- codex_flow/__main__.py +9 -0
- codex_flow/cli.py +242 -0
- codex_flow/data/LICENSE +21 -0
- codex_flow/data/README.en.md +303 -0
- codex_flow/data/README.md +305 -0
- codex_flow/data/VERSION +1 -0
- codex_flow/data/apps/chatgpt-mcp/README.md +86 -0
- codex_flow/data/apps/chatgpt-mcp/__init__.py +1 -0
- codex_flow/data/apps/chatgpt-mcp/adapter.py +458 -0
- codex_flow/data/apps/chatgpt-mcp/server.py +358 -0
- codex_flow/data/apps/chatgpt-mcp/widget.html +927 -0
- codex_flow/data/apps/macos-overlay/README.en.md +121 -0
- codex_flow/data/apps/macos-overlay/README.md +123 -0
- codex_flow/data/apps/macos-overlay/Sources/Controllers/OverlayRuntimeState.swift +126 -0
- codex_flow/data/apps/macos-overlay/Sources/Controllers/OverlayScreenGeometry.swift +82 -0
- codex_flow/data/apps/macos-overlay/Sources/Controllers/OverlayWindowController.swift +1052 -0
- codex_flow/data/apps/macos-overlay/Sources/Localization.swift +197 -0
- codex_flow/data/apps/macos-overlay/Sources/Models/TelemetryData.swift +1557 -0
- codex_flow/data/apps/macos-overlay/Sources/Services/AccountSnapshotService.swift +1101 -0
- codex_flow/data/apps/macos-overlay/Sources/Services/FlowPilotInstanceLock.swift +153 -0
- codex_flow/data/apps/macos-overlay/Sources/Services/IPCServer.swift +298 -0
- codex_flow/data/apps/macos-overlay/Sources/Services/TelemetryQueryEngine.swift +800 -0
- codex_flow/data/apps/macos-overlay/Sources/Services/TelemetryWatcher.swift +135 -0
- codex_flow/data/apps/macos-overlay/Sources/Services/UpdateService.swift +610 -0
- codex_flow/data/apps/macos-overlay/Sources/Views/AccountView.swift +610 -0
- codex_flow/data/apps/macos-overlay/Sources/Views/AnalyticsView.swift +566 -0
- codex_flow/data/apps/macos-overlay/Sources/Views/AutostartView.swift +293 -0
- codex_flow/data/apps/macos-overlay/Sources/Views/BubbleView.swift +317 -0
- codex_flow/data/apps/macos-overlay/Sources/Views/HistoryView.swift +1124 -0
- codex_flow/data/apps/macos-overlay/Sources/Views/HoverRevealText.swift +165 -0
- codex_flow/data/apps/macos-overlay/Sources/Views/InspectorSkillsToolsView.swift +121 -0
- codex_flow/data/apps/macos-overlay/Sources/Views/LogoView.swift +182 -0
- codex_flow/data/apps/macos-overlay/Sources/Views/SleekSwitch.swift +117 -0
- codex_flow/data/apps/macos-overlay/Sources/Views/StrategyModeView.swift +561 -0
- codex_flow/data/apps/macos-overlay/Sources/Views/SummaryView.swift +1273 -0
- codex_flow/data/apps/macos-overlay/Sources/Views/UpdateView.swift +352 -0
- codex_flow/data/apps/macos-overlay/Sources/main.swift +340 -0
- codex_flow/data/apps/macos-overlay/Tests/OverlayScreenGeometryTests.swift +163 -0
- codex_flow/data/apps/macos-overlay/Tests/TelemetryPhase1ContractTests.swift +357 -0
- codex_flow/data/apps/macos-overlay/Tests/TelemetryQueryEngineConcurrencyTests.swift +221 -0
- codex_flow/data/apps/macos-overlay/Tests/TelemetryQuotaSelectionTests.swift +158 -0
- codex_flow/data/apps/macos-overlay/Tests/TelemetryWorkerTokenTests.swift +122 -0
- codex_flow/data/apps/macos-overlay/build.sh +75 -0
- codex_flow/data/benchmark/corpus.json +103 -0
- codex_flow/data/benchmark/manifest.example.json +41 -0
- codex_flow/data/benchmark/manifest.schema.json +137 -0
- codex_flow/data/benchmark/prices/gpt-5.6-2026-08-30.json +5 -0
- codex_flow/data/benchmark/profiles.json +90 -0
- codex_flow/data/benchmark/schema.json +77 -0
- codex_flow/data/benchmark/tasks.json +50 -0
- codex_flow/data/completions/codex-flow.bash +34 -0
- codex_flow/data/completions/codex-flow.zsh +52 -0
- codex_flow/data/glama.json +6 -0
- codex_flow/data/install-release.ps1 +126 -0
- codex_flow/data/install-release.sh +155 -0
- codex_flow/data/install.ps1 +349 -0
- codex_flow/data/install.sh +362 -0
- codex_flow/data/policy/benchmark.toml +49 -0
- codex_flow/data/policy/defaults.toml +70 -0
- codex_flow/data/scripts/analyze-benchmark.py +510 -0
- codex_flow/data/scripts/benchmark-local.py +171 -0
- codex_flow/data/scripts/check-recommendation.py +277 -0
- codex_flow/data/scripts/doctor.py +449 -0
- codex_flow/data/scripts/generate-release-manifest.py +74 -0
- codex_flow/data/scripts/localization.py +192 -0
- codex_flow/data/scripts/manage-hooks.py +448 -0
- codex_flow/data/scripts/manage-instructions.py +389 -0
- codex_flow/data/scripts/manage-shell.py +151 -0
- codex_flow/data/scripts/materialize-corpus.py +193 -0
- codex_flow/data/scripts/menu.py +646 -0
- codex_flow/data/scripts/migrations/0001_update_settings.py +80 -0
- codex_flow/data/scripts/package-release.py +132 -0
- codex_flow/data/scripts/render-benchmark-report.py +292 -0
- codex_flow/data/scripts/run-benchmark.py +829 -0
- codex_flow/data/scripts/strategies/__init__.py +28 -0
- codex_flow/data/scripts/strategies/balanced.py +115 -0
- codex_flow/data/scripts/strategies/base.py +363 -0
- codex_flow/data/scripts/strategies/efficient.py +158 -0
- codex_flow/data/scripts/strategies/lifecycle_runtime.py +590 -0
- codex_flow/data/scripts/strategies/quality.py +209 -0
- codex_flow/data/scripts/strategies/speed.py +108 -0
- codex_flow/data/scripts/strategies/task_budget_runtime.py +644 -0
- codex_flow/data/scripts/strategies/task_phase_runtime.py +341 -0
- codex_flow/data/scripts/strategies/work_unit_runtime.py +421 -0
- codex_flow/data/scripts/strategy_runtime.py +1091 -0
- codex_flow/data/scripts/telemetry.py +400 -0
- codex_flow/data/scripts/telemetry_core/__init__.py +192 -0
- codex_flow/data/scripts/telemetry_core/app_server.py +1192 -0
- codex_flow/data/scripts/telemetry_core/collector.py +1247 -0
- codex_flow/data/scripts/telemetry_core/common.py +421 -0
- codex_flow/data/scripts/telemetry_core/latency.py +593 -0
- codex_flow/data/scripts/telemetry_core/query.py +427 -0
- codex_flow/data/scripts/telemetry_core/quota_ledger.py +598 -0
- codex_flow/data/scripts/telemetry_core/render.py +460 -0
- codex_flow/data/scripts/telemetry_core/repair.py +223 -0
- codex_flow/data/scripts/ui.py +266 -0
- codex_flow/data/scripts/update-homebrew-formula.py +146 -0
- codex_flow/data/scripts/update_runtime_config.py +134 -0
- codex_flow/data/scripts/updater.py +1718 -0
- codex_flow/data/smithery.yaml +18 -0
- codex_flow/data/templates/agents/worker-explorer.toml +24 -0
- codex_flow/data/templates/agents/worker-implementer.toml +49 -0
- codex_flow/data/templates/agents/worker-reviewer.toml +25 -0
- codex_flow/data/templates/flow-pilot-instructions.md +35 -0
- codex_flow/data/templates/skills/flow-pilot/SKILL.md +577 -0
- codex_flow/mcp.py +35 -0
- codex_flow-2.1.13.dist-info/METADATA +342 -0
- codex_flow-2.1.13.dist-info/RECORD +113 -0
- codex_flow-2.1.13.dist-info/WHEEL +5 -0
- codex_flow-2.1.13.dist-info/entry_points.txt +3 -0
- codex_flow-2.1.13.dist-info/licenses/LICENSE +21 -0
- 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
|
+
}
|