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,1124 @@
1
+ import SwiftUI
2
+
3
+ public struct HistoryView: View {
4
+ @ObservedObject var state: OverlayState
5
+ @ObservedObject private var localization = AppLocalization.shared
6
+ public var isFullHeight: Bool = false
7
+
8
+ @State private var availableProjects: [String] = []
9
+ @State private var detailRun: TaskRun?
10
+ @State private var detailHoverGeneration = 0
11
+ @State private var searchGeneration = 0
12
+
13
+ public init(state: OverlayState, isFullHeight: Bool = false) {
14
+ self.state = state
15
+ self.isFullHeight = isFullHeight
16
+ }
17
+
18
+ public var body: some View {
19
+ ZStack(alignment: .topTrailing) {
20
+ historySurface
21
+ detailOverlay
22
+ }
23
+ .onAppear(perform: loadInitialHistory)
24
+ }
25
+
26
+ private var historySurface: some View {
27
+ VStack(spacing: 8) {
28
+ filterBar
29
+ if state.historyChats.isEmpty && state.historyRuns.isEmpty {
30
+ emptyState
31
+ } else {
32
+ historyList
33
+ }
34
+ }
35
+ }
36
+
37
+ @ViewBuilder
38
+ private var detailOverlay: some View {
39
+ if let run = detailRun {
40
+ HistoryTaskDetailOverlay(
41
+ run: run,
42
+ isPrivacyMode: state.isPrivacyMode,
43
+ onClose: closeDetail
44
+ )
45
+ .frame(width: 350)
46
+ .padding(.top, 34)
47
+ .padding(.trailing, 2)
48
+ .zIndex(20)
49
+ .transition(.opacity.combined(with: .scale(scale: 0.97, anchor: .topTrailing)))
50
+ .onHover { inside in
51
+ inside ? cancelScheduledClose() : scheduleDetailClose()
52
+ }
53
+ }
54
+ }
55
+
56
+ // MARK: - Filters
57
+
58
+ private var filterBar: some View {
59
+ VStack(spacing: 6) {
60
+ HStack(spacing: 6) {
61
+ scopeControls
62
+ projectMenu
63
+ Spacer()
64
+ refreshButton
65
+ }
66
+ searchField
67
+ }
68
+ .padding(.top, 7)
69
+ }
70
+
71
+ private var scopeControls: some View {
72
+ HStack(spacing: 2) {
73
+ scopeButton(title: L("All", "全部"), selected: !state.isTodayOnly) {
74
+ state.isTodayOnly = false
75
+ state.loadHistory()
76
+ }
77
+ scopeButton(title: L("Today", "今天"), selected: state.isTodayOnly) {
78
+ state.isTodayOnly = true
79
+ state.loadHistory()
80
+ }
81
+ }
82
+ .padding(2)
83
+ .background(Capsule().fill(Color.white.opacity(0.06)))
84
+ }
85
+
86
+ @ViewBuilder
87
+ private var projectMenu: some View {
88
+ if availableProjects.count > 2 {
89
+ Menu {
90
+ ForEach(availableProjects, id: \.self) { project in
91
+ Button(project == "All" ? L("All Projects", "全部项目") : project) {
92
+ state.selectedProject = project == "All" ? nil : project
93
+ state.loadHistory()
94
+ }
95
+ }
96
+ } label: {
97
+ HStack(spacing: 3) {
98
+ Image(systemName: "folder")
99
+ .font(.system(size: 8))
100
+ .foregroundColor(.cyan.opacity(0.85))
101
+ HoverRevealText(
102
+ state.selectedProject ?? L("All Projects", "全部项目"),
103
+ font: .system(size: 8.5, weight: .medium),
104
+ foregroundColor: .white.opacity(0.72),
105
+ lineLimit: 1,
106
+ privacyBlur: state.isPrivacyMode && state.selectedProject != nil,
107
+ popoverWidth: 320
108
+ )
109
+ .frame(maxWidth: 118, alignment: .leading)
110
+ Image(systemName: "chevron.down")
111
+ .font(.system(size: 6.5))
112
+ .foregroundColor(.white.opacity(0.4))
113
+ }
114
+ .padding(.horizontal, 6)
115
+ .padding(.vertical, 3)
116
+ .background(Capsule().fill(Color.white.opacity(0.05)))
117
+ }
118
+ .menuStyle(.borderlessButton)
119
+ }
120
+ }
121
+
122
+ private var refreshButton: some View {
123
+ Button {
124
+ state.loadHistory()
125
+ } label: {
126
+ Image(systemName: "arrow.clockwise")
127
+ .font(.system(size: 8.5, weight: .semibold))
128
+ .foregroundColor(.white.opacity(0.55))
129
+ .frame(width: 20, height: 20)
130
+ .background(Circle().fill(Color.white.opacity(0.05)))
131
+ }
132
+ .buttonStyle(.plain)
133
+ }
134
+
135
+ private var searchField: some View {
136
+ HStack(spacing: 5) {
137
+ Image(systemName: "magnifyingglass")
138
+ .font(.system(size: 8.5))
139
+ .foregroundColor(.white.opacity(0.4))
140
+
141
+ TextField(L("Search chat or session…", "搜索对话或任务…"), text: $state.searchQuery)
142
+ .textFieldStyle(.plain)
143
+ .font(.system(size: 9.2))
144
+ .foregroundColor(.white)
145
+ .onChange(of: state.searchQuery) { _, _ in scheduleSearchReload() }
146
+
147
+ if !state.searchQuery.isEmpty {
148
+ Button {
149
+ state.searchQuery = ""
150
+ } label: {
151
+ Image(systemName: "xmark.circle.fill")
152
+ .font(.system(size: 8.5))
153
+ .foregroundColor(.white.opacity(0.4))
154
+ }
155
+ .buttonStyle(.plain)
156
+ }
157
+ }
158
+ .padding(.horizontal, 7)
159
+ .padding(.vertical, 5)
160
+ .background(
161
+ RoundedRectangle(cornerRadius: 7)
162
+ .fill(Color.white.opacity(0.035))
163
+ .overlay(RoundedRectangle(cornerRadius: 7).stroke(Color.white.opacity(0.075), lineWidth: 0.6))
164
+ )
165
+ }
166
+
167
+ private func scopeButton(title: String, selected: Bool, action: @escaping () -> Void) -> some View {
168
+ Button(action: action) {
169
+ Text(title)
170
+ .font(.system(size: 8.2, weight: selected ? .bold : .medium, design: .rounded))
171
+ .foregroundColor(selected ? .white : .white.opacity(0.46))
172
+ .padding(.horizontal, 6)
173
+ .padding(.vertical, 2)
174
+ .background(Capsule().fill(selected ? Color.cyan.opacity(0.28) : Color.clear))
175
+ }
176
+ .buttonStyle(.plain)
177
+ }
178
+
179
+ // MARK: - History List
180
+
181
+ private var historyList: some View {
182
+ Group {
183
+ if isFullHeight {
184
+ historyContent
185
+ } else {
186
+ ScrollView(.vertical, showsIndicators: true) { historyContent }
187
+ .frame(maxHeight: 355)
188
+ }
189
+ }
190
+ }
191
+
192
+ private var historyContent: some View {
193
+ LazyVStack(spacing: 7) {
194
+ if !state.historyChats.isEmpty {
195
+ ForEach(Array(state.historyChats.enumerated()), id: \.element.id) { index, chat in
196
+ HistoryChatRow(
197
+ index: index + 1,
198
+ chat: chat,
199
+ expanded: state.isChatExpanded(chat.sessionId),
200
+ selectedRunId: detailRun?.id,
201
+ isPrivacyMode: state.isPrivacyMode,
202
+ onToggle: { state.toggleChatExpansion(chat.sessionId) },
203
+ onSelectRun: showDetail
204
+ )
205
+ }
206
+ } else {
207
+ ForEach(Array(state.historyRuns.enumerated()), id: \.element.id) { index, run in
208
+ HistoryStandaloneRunRow(
209
+ index: index + 1,
210
+ run: run,
211
+ selected: detailRun?.id == run.id,
212
+ isPrivacyMode: state.isPrivacyMode,
213
+ onSelect: { showDetail(run) }
214
+ )
215
+ }
216
+ }
217
+ }
218
+ .padding(.vertical, 2)
219
+ }
220
+
221
+ private var emptyState: some View {
222
+ VStack(spacing: 8) {
223
+ Image(systemName: state.searchQuery.isEmpty ? "tray" : "magnifyingglass")
224
+ .font(.system(size: 23))
225
+ .foregroundColor(.white.opacity(0.28))
226
+ .padding(.top, 28)
227
+
228
+ Text(state.searchQuery.isEmpty ? L("No telemetry history yet", "尚无遥测历史") : L("No matching tasks", "没有匹配的任务"))
229
+ .font(.system(size: 11, weight: .semibold, design: .rounded))
230
+ .foregroundColor(.white.opacity(0.72))
231
+
232
+ if !state.searchQuery.isEmpty {
233
+ Button(L("Clear Search", "清除搜索")) { state.searchQuery = "" }
234
+ .buttonStyle(.plain)
235
+ .font(.system(size: 9, weight: .semibold))
236
+ .foregroundColor(.cyan)
237
+ }
238
+ }
239
+ .frame(maxWidth: .infinity, minHeight: 230)
240
+ }
241
+
242
+ // MARK: - Local task detail overlay
243
+
244
+ private func showDetail(_ run: TaskRun) {
245
+ cancelScheduledClose()
246
+ withAnimation(.easeOut(duration: 0.15)) { detailRun = run }
247
+
248
+ if run.trajectory == nil || run.skillsUsed == nil || run.toolsUsed == nil || run.logs == nil {
249
+ var enriched = run
250
+ DispatchQueue.global(qos: .userInitiated).async {
251
+ TelemetryQueryEngine.shared.enrichRunIfNeeded(&enriched)
252
+ DispatchQueue.main.async {
253
+ guard detailRun?.id == enriched.id else { return }
254
+ detailRun = enriched
255
+ }
256
+ }
257
+ }
258
+ }
259
+
260
+ private func loadInitialHistory() {
261
+ availableProjects = ["All"] + TelemetryQueryEngine.shared.allProjects()
262
+ if state.historyChats.isEmpty && state.historyRuns.isEmpty { state.loadHistory() }
263
+ }
264
+
265
+ private func closeDetail() {
266
+ cancelScheduledClose()
267
+ withAnimation(.easeIn(duration: 0.12)) { detailRun = nil }
268
+ }
269
+
270
+ private func cancelScheduledClose() { detailHoverGeneration += 1 }
271
+
272
+ private func scheduleDetailClose() {
273
+ detailHoverGeneration += 1
274
+ let generation = detailHoverGeneration
275
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.32) {
276
+ guard generation == detailHoverGeneration else { return }
277
+ withAnimation(.easeIn(duration: 0.12)) { detailRun = nil }
278
+ }
279
+ }
280
+
281
+ private func scheduleSearchReload() {
282
+ searchGeneration += 1
283
+ let generation = searchGeneration
284
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.18) {
285
+ guard generation == searchGeneration else { return }
286
+ state.loadHistory()
287
+ }
288
+ }
289
+ }
290
+
291
+ // MARK: - Chat accordion
292
+
293
+ public struct HistoryChatRow: View {
294
+ public let index: Int
295
+ public let chat: ChatSession
296
+ public let expanded: Bool
297
+ public let selectedRunId: String?
298
+ public let isPrivacyMode: Bool
299
+ public let onToggle: () -> Void
300
+ public let onSelectRun: (TaskRun) -> Void
301
+
302
+ @State private var hovered = false
303
+
304
+ public var body: some View {
305
+ VStack(spacing: 0) {
306
+ headerButton
307
+ if expanded { expandedTurns }
308
+ }
309
+ .background(rowBackground)
310
+ .overlay(rowBorder)
311
+ .clipShape(RoundedRectangle(cornerRadius: 9))
312
+ }
313
+
314
+ private var headerButton: some View {
315
+ Button(action: onToggle) {
316
+ HStack(alignment: .top, spacing: 7) {
317
+ indexColumn
318
+ chatSummary
319
+ }
320
+ .padding(.horizontal, 7)
321
+ .padding(.vertical, 7)
322
+ .contentShape(Rectangle())
323
+ }
324
+ .buttonStyle(.plain)
325
+ .onHover { hovered = $0 }
326
+ }
327
+
328
+ private var indexColumn: some View {
329
+ VStack(spacing: 4) {
330
+ Text("#\(index)")
331
+ .font(.system(size: 9, weight: .bold, design: .monospaced))
332
+ .foregroundColor(index == 1 ? .cyan : .white.opacity(0.45))
333
+ Image(systemName: "chevron.right")
334
+ .font(.system(size: 7.5, weight: .semibold))
335
+ .foregroundColor(.white.opacity(0.42))
336
+ .rotationEffect(.degrees(expanded ? 90 : 0))
337
+ }
338
+ .frame(width: 25)
339
+ }
340
+
341
+ private var chatSummary: some View {
342
+ VStack(alignment: .leading, spacing: 3) {
343
+ projectRow
344
+ HoverRevealText(
345
+ chat.title,
346
+ font: .system(size: 9.3),
347
+ foregroundColor: .white.opacity(0.78),
348
+ lineLimit: 1,
349
+ privacyBlur: isPrivacyMode,
350
+ popoverWidth: 400
351
+ )
352
+ .frame(maxWidth: .infinity, alignment: .leading)
353
+ metadataRow
354
+ }
355
+ }
356
+
357
+ private var projectRow: some View {
358
+ HStack(spacing: 4) {
359
+ HoverRevealText(
360
+ chat.projectName,
361
+ font: .system(size: 9.8, weight: .bold, design: .rounded),
362
+ foregroundColor: .white.opacity(0.92),
363
+ lineLimit: 1,
364
+ privacyBlur: isPrivacyMode,
365
+ popoverWidth: 300
366
+ )
367
+
368
+ if chat.isInternalTask {
369
+ Text(L("System", "系统"))
370
+ .font(.system(size: 7, weight: .semibold))
371
+ .foregroundColor(.white.opacity(0.55))
372
+ .padding(.horizontal, 3.5)
373
+ .padding(.vertical, 1)
374
+ .background(Color.white.opacity(0.1))
375
+ .clipShape(RoundedRectangle(cornerRadius: 3))
376
+ }
377
+
378
+ if let branch = chat.gitBranch, !branch.isEmpty {
379
+ HoverRevealText(
380
+ "(\(branch))",
381
+ font: .system(size: 8, weight: .medium, design: .monospaced),
382
+ foregroundColor: .cyan.opacity(0.78),
383
+ lineLimit: 1,
384
+ privacyBlur: isPrivacyMode,
385
+ popoverWidth: 300
386
+ )
387
+ }
388
+
389
+ Spacer(minLength: 4)
390
+ Text(chat.localizedFormattedDate)
391
+ .font(.system(size: 7.8))
392
+ .foregroundColor(.white.opacity(0.38))
393
+ }
394
+ }
395
+
396
+ private var metadataRow: some View {
397
+ HStack(spacing: 6) {
398
+ Label(chat.localizedRunsSummary, systemImage: "bubble.left.and.bubble.right.fill")
399
+ .font(.system(size: 7.8, weight: .semibold))
400
+ .foregroundColor(.cyan.opacity(0.8))
401
+
402
+ Text(chat.formattedDuration)
403
+ .font(.system(size: 7.8, design: .monospaced))
404
+ .foregroundColor(.white.opacity(0.45))
405
+
406
+ workerCount
407
+ Spacer()
408
+ quotaDelta
409
+
410
+ if chat.isAborted && chat.totalTokens == 0 {
411
+ Text(L("Interrupted", "已中断"))
412
+ .font(.system(size: 7.5, weight: .medium))
413
+ .foregroundColor(.white.opacity(0.45))
414
+ .padding(.horizontal, 3.5)
415
+ .padding(.vertical, 1)
416
+ .background(Color.orange.opacity(0.15))
417
+ .clipShape(RoundedRectangle(cornerRadius: 3))
418
+ } else {
419
+ Text(chat.formattedTotalTokens)
420
+ .font(.system(size: 8.5, weight: .bold, design: .rounded))
421
+ .foregroundColor(Color(red: 0.95, green: 0.35, blue: 0.8))
422
+ .help(chat.workerTotalTokens > 0
423
+ ? String(format: L("Total: %@ (Parent: %@, Worker: %@)", "总计 %@(父级:%@,Worker:%@)"),
424
+ chat.formattedTotalTokens,
425
+ TaskRun.formatTokenCount(chat.parentTotalTokens),
426
+ chat.formattedWorkerTokens)
427
+ : String(format: L("Total tokens: %@", "总 Token:%@"), chat.formattedTotalTokens)
428
+ )
429
+ }
430
+
431
+ Circle()
432
+ .fill(statusColor)
433
+ .frame(width: 4.5, height: 4.5)
434
+ }
435
+ }
436
+
437
+ @ViewBuilder
438
+ private var workerCount: some View {
439
+ if chat.maxWorkerCount > 0 {
440
+ HStack(spacing: 2.5) {
441
+ Label("\(chat.maxWorkerCount)w", systemImage: "person.2.fill")
442
+ .font(.system(size: 7.6, weight: .medium))
443
+ if chat.workerTotalTokens > 0 {
444
+ Text("(\(chat.formattedWorkerTokens))")
445
+ .font(.system(size: 7.2, weight: .medium, design: .monospaced))
446
+ }
447
+ }
448
+ .foregroundColor(.teal.opacity(0.85))
449
+ .help(chat.workerTotalTokens > 0
450
+ ? String(format: L("Worker tokens: %@ (%@ of total)", "Worker 消耗:%@(占总量的 %@)"), chat.formattedWorkerTokens, chat.formattedWorkerSharePercent)
451
+ : String(format: L("%d workers max", "最高并发 %d 个 Worker"), chat.maxWorkerCount)
452
+ )
453
+ }
454
+ }
455
+
456
+ @ViewBuilder
457
+ private var quotaDelta: some View {
458
+ if let delta = chat.totalQuotaDelta {
459
+ let formatted = abs(delta) < 0.95
460
+ ? String(format: "%+.1f%%", -delta)
461
+ : String(format: "%+.0f%%", -delta)
462
+ let deltaMagnitude = abs(delta) < 0.95
463
+ ? String(format: "%.1f%%", abs(delta))
464
+ : String(format: "%.0f%%", abs(delta))
465
+ if abs(delta) >= 0.1 {
466
+ let helpText = chat.isPartiallyAttributed
467
+ ? String(format: L("Partially attributed quota: %@ (subtotal, some runs pending)", "部分已归因配额:%@(小计,部分任务待归因)"), deltaMagnitude)
468
+ : (delta > 0
469
+ ? String(format: L("Quota consumed (Estimated): %@", "配额消耗(估算):%@"), deltaMagnitude)
470
+ : String(format: L("Quota restored (Estimated): %@", "配额恢复(估算):%@"), deltaMagnitude))
471
+ Text("≈\(formatted)")
472
+ .font(.system(size: 7.5, weight: .bold))
473
+ .foregroundColor(delta > 0 ? .orange : .green)
474
+ .help(helpText)
475
+ } else {
476
+ let helpText = chat.isPartiallyAttributed
477
+ ? L("Partially attributed quota: 0.0% (subtotal, some runs pending)", "部分已归因配额:0.0%(小计,部分任务待归因)")
478
+ : L("Quota consumed (Estimated): 0.0%", "配额消耗(估算):0.0%")
479
+ Text("≈0%")
480
+ .font(.system(size: 7.5, weight: .medium))
481
+ .foregroundColor(.white.opacity(0.4))
482
+ .help(helpText)
483
+ }
484
+ } else if let obs = chat.observedAccountDelta, abs(obs) >= 0.1 {
485
+ let formatted = abs(obs) < 0.95
486
+ ? String(format: "%+.1f%%", -obs)
487
+ : String(format: "%+.0f%%", -obs)
488
+ let deltaMagnitude = abs(obs) < 0.95
489
+ ? String(format: "%.1f%%", abs(obs))
490
+ : String(format: "%.0f%%", abs(obs))
491
+ Text(formatted)
492
+ .font(.system(size: 7.5, weight: .medium))
493
+ .foregroundColor(.white.opacity(0.45))
494
+ .help(String(format: L("Observed account change during execution: %@ (Attribution pending)", "执行期间账户变化:%@(归因待完成)"), deltaMagnitude))
495
+ }
496
+ }
497
+
498
+ private var statusColor: Color {
499
+ if chat.isRunning { return .cyan }
500
+ if chat.isAborted { return .orange }
501
+ if chat.isError { return .red }
502
+ return .green
503
+ }
504
+
505
+ private var expandedTurns: some View {
506
+ VStack(spacing: 0) {
507
+ Divider().background(Color.white.opacity(0.07))
508
+ VStack(spacing: 3) {
509
+ turnsHeader
510
+ turnsList
511
+ }
512
+ .padding(.horizontal, 5)
513
+ .padding(.bottom, 5)
514
+ .background(Color.black.opacity(0.16))
515
+ }
516
+ }
517
+
518
+ private var turnsHeader: some View {
519
+ HStack {
520
+ Text(L("Session Turns", "执行轮次"))
521
+ .font(.system(size: 7.8, weight: .bold, design: .rounded))
522
+ .foregroundColor(.white.opacity(0.42))
523
+ Spacer()
524
+ if let latest = chat.latestRun {
525
+ Button { onSelectRun(latest) } label: {
526
+ Label(L("Details", "详情"), systemImage: "rectangle.inset.filled.and.person.filled")
527
+ .font(.system(size: 7.5, weight: .semibold))
528
+ .foregroundColor(.cyan)
529
+ }
530
+ .buttonStyle(.plain)
531
+ }
532
+ }
533
+ .padding(.horizontal, 7)
534
+ .padding(.top, 5)
535
+ }
536
+
537
+ private var turnsList: some View {
538
+ ForEach(Array(chat.runs.enumerated()), id: \.element.id) { offset, run in
539
+ HistoryRunRow(
540
+ turnNumber: chat.runs.count - offset,
541
+ run: run,
542
+ selected: selectedRunId == run.id,
543
+ isPrivacyMode: isPrivacyMode,
544
+ onSelect: { onSelectRun(run) }
545
+ )
546
+ }
547
+ }
548
+
549
+ private var rowBackground: some View {
550
+ RoundedRectangle(cornerRadius: 9)
551
+ .fill(hovered ? Color.white.opacity(0.055) : Color.white.opacity(0.03))
552
+ }
553
+
554
+ private var rowBorder: some View {
555
+ RoundedRectangle(cornerRadius: 9)
556
+ .stroke(expanded ? Color.cyan.opacity(0.22) : Color.white.opacity(hovered ? 0.12 : 0.065), lineWidth: 0.7)
557
+ }
558
+ }
559
+
560
+ public struct HistoryRunRow: View {
561
+ public let turnNumber: Int
562
+ public let run: TaskRun
563
+ public let selected: Bool
564
+ public let isPrivacyMode: Bool
565
+ public let onSelect: () -> Void
566
+
567
+ @State private var hovered = false
568
+
569
+ public var body: some View {
570
+ Button(action: onSelect) {
571
+ HStack(spacing: 5) {
572
+ selectionBar
573
+ turnLabel
574
+ runSummary
575
+ }
576
+ .padding(.horizontal, 5)
577
+ .padding(.vertical, 4)
578
+ .background(rowBackground)
579
+ .overlay(rowBorder)
580
+ }
581
+ .buttonStyle(.plain)
582
+ .onHover { hovered = $0 }
583
+ }
584
+
585
+ private var selectionBar: some View {
586
+ Capsule()
587
+ .fill(selected ? Color.cyan : (run.isAborted ? Color.orange.opacity(0.6) : Color.white.opacity(hovered ? 0.35 : 0.12)))
588
+ .frame(width: 2.5, height: 24)
589
+ }
590
+
591
+ private var turnLabel: some View {
592
+ Text("#\(turnNumber)")
593
+ .font(.system(size: 7.8, weight: .bold, design: .monospaced))
594
+ .foregroundColor(selected ? .cyan : .white.opacity(0.45))
595
+ .frame(width: 22, alignment: .leading)
596
+ }
597
+
598
+ private var runSummary: some View {
599
+ VStack(alignment: .leading, spacing: 2) {
600
+ runMetadata
601
+ let preview = run.turnPreview
602
+ if !preview.isEmpty {
603
+ HoverRevealText(
604
+ preview,
605
+ font: .system(size: 8.2),
606
+ foregroundColor: .white.opacity(0.6),
607
+ lineLimit: 1,
608
+ privacyBlur: isPrivacyMode,
609
+ popoverWidth: 400
610
+ )
611
+ .frame(maxWidth: .infinity, alignment: .leading)
612
+ }
613
+ }
614
+ }
615
+
616
+ private var runMetadata: some View {
617
+ HStack(spacing: 4) {
618
+ Text(run.localizedFormattedDate)
619
+ .font(.system(size: 7.4))
620
+ .foregroundColor(.white.opacity(0.38))
621
+ Text("·").foregroundColor(.white.opacity(0.2))
622
+ Text(run.formattedDuration)
623
+ .font(.system(size: 7.4, design: .monospaced))
624
+ .foregroundColor(.white.opacity(0.5))
625
+ if run.isInternalTask {
626
+ Text(L("System", "系统"))
627
+ .font(.system(size: 6.8, weight: .semibold))
628
+ .foregroundColor(.white.opacity(0.55))
629
+ .padding(.horizontal, 3)
630
+ .padding(.vertical, 0.5)
631
+ .background(Color.white.opacity(0.1))
632
+ .clipShape(RoundedRectangle(cornerRadius: 3))
633
+ }
634
+ Spacer()
635
+ if !run.allWorkers.isEmpty {
636
+ let workerText = run.workerTotalTokens > 0 ? "\(run.allWorkers.count)w (\(run.formattedWorkerTokens))" : "\(run.allWorkers.count)w"
637
+ Text(workerText)
638
+ .font(.system(size: 7.2, weight: .semibold))
639
+ .foregroundColor(.teal.opacity(0.85))
640
+ .help(run.workerTotalTokens > 0
641
+ ? String(format: L("Worker: %@ (%@ of turn)", "Worker 消耗:%@(占本轮 %@)"), run.formattedWorkerTokens, run.formattedWorkerSharePercent)
642
+ : String(format: L("%d workers", "%d 个 Worker"), run.allWorkers.count)
643
+ )
644
+ }
645
+ quotaDelta
646
+ if run.isAborted && run.totalTokens == 0 {
647
+ Text(L("Interrupted", "已中断"))
648
+ .font(.system(size: 7.2, weight: .medium))
649
+ .foregroundColor(.white.opacity(0.45))
650
+ .padding(.horizontal, 3)
651
+ .padding(.vertical, 0.5)
652
+ .background(Color.orange.opacity(0.15))
653
+ .clipShape(RoundedRectangle(cornerRadius: 3))
654
+ } else {
655
+ Text(run.formattedTotalTokens)
656
+ .font(.system(size: 7.8, weight: .bold, design: .rounded))
657
+ .foregroundColor(Color(red: 0.95, green: 0.35, blue: 0.8))
658
+ .help(run.workerTotalTokens > 0
659
+ ? String(format: L("Total: %@ (Parent: %@, Worker: %@)", "总计 %@(父级:%@,Worker:%@)"),
660
+ run.formattedTotalTokens,
661
+ TaskRun.formatTokenCount(run.parentTotalTokens),
662
+ run.formattedWorkerTokens)
663
+ : String(format: L("Total tokens: %@", "总 Token:%@"), run.formattedTotalTokens)
664
+ )
665
+ }
666
+ }
667
+ }
668
+
669
+ @ViewBuilder
670
+ private var quotaDelta: some View {
671
+ if let delta = run.canonicalQuotaDelta {
672
+ let formatted = abs(delta) < 0.95
673
+ ? String(format: "%+.1f%%", -delta)
674
+ : String(format: "%+.0f%%", -delta)
675
+ let deltaMagnitude = abs(delta) < 0.95
676
+ ? String(format: "%.1f%%", abs(delta))
677
+ : String(format: "%.0f%%", abs(delta))
678
+ if abs(delta) >= 0.1 {
679
+ Text("≈\(formatted)")
680
+ .font(.system(size: 7.2, weight: .bold))
681
+ .foregroundColor(delta > 0 ? .orange : .green)
682
+ .help(delta > 0
683
+ ? String(format: L("Quota consumed (Estimated): %@", "配额消耗(估算):%@"), deltaMagnitude)
684
+ : String(format: L("Quota restored (Estimated): %@", "配额恢复(估算):%@"), deltaMagnitude)
685
+ )
686
+ } else {
687
+ Text("≈0%")
688
+ .font(.system(size: 7.2, weight: .medium))
689
+ .foregroundColor(.white.opacity(0.4))
690
+ .help(L("Quota consumed (Estimated): 0.0%", "配额消耗(估算):0.0%"))
691
+ }
692
+ } else if let obs = run.observedAccountDelta, abs(obs) >= 0.1 {
693
+ let formatted = abs(obs) < 0.95
694
+ ? String(format: "%+.1f%%", -obs)
695
+ : String(format: "%+.0f%%", -obs)
696
+ let deltaMagnitude = abs(obs) < 0.95
697
+ ? String(format: "%.1f%%", abs(obs))
698
+ : String(format: "%.0f%%", abs(obs))
699
+ Text(formatted)
700
+ .font(.system(size: 7.2, weight: .medium))
701
+ .foregroundColor(.white.opacity(0.45))
702
+ .help(String(format: L("Observed account change during execution: %@ (Attribution pending)", "执行期间账户变化:%@(归因待完成)"), deltaMagnitude))
703
+ }
704
+ }
705
+
706
+ private var rowBackground: some View {
707
+ RoundedRectangle(cornerRadius: 6)
708
+ .fill(selected ? Color.cyan.opacity(0.14) : (hovered ? Color.white.opacity(0.06) : Color.white.opacity(0.018)))
709
+ }
710
+
711
+ private var rowBorder: some View {
712
+ RoundedRectangle(cornerRadius: 6)
713
+ .stroke(selected ? Color.cyan.opacity(0.42) : Color.clear, lineWidth: 0.7)
714
+ }
715
+ }
716
+
717
+ public struct HistoryStandaloneRunRow: View {
718
+ public let index: Int
719
+ public let run: TaskRun
720
+ public let selected: Bool
721
+ public let isPrivacyMode: Bool
722
+ public let onSelect: () -> Void
723
+
724
+ public var body: some View {
725
+ HistoryRunRow(
726
+ turnNumber: index,
727
+ run: run,
728
+ selected: selected,
729
+ isPrivacyMode: isPrivacyMode,
730
+ onSelect: onSelect
731
+ )
732
+ }
733
+ }
734
+
735
+ // MARK: - History-local detail surface
736
+
737
+ public struct HistoryTaskDetailOverlay: View {
738
+ public let run: TaskRun
739
+ public let isPrivacyMode: Bool
740
+ public let onClose: () -> Void
741
+ @State private var executionExpanded = false
742
+ @State private var logsExpanded = false
743
+
744
+ public var body: some View {
745
+ VStack(spacing: 7) {
746
+ detailTitleBar
747
+ detailScroll
748
+ }
749
+ .padding(9)
750
+ .background(detailBackground)
751
+ .overlay(
752
+ RoundedRectangle(cornerRadius: 13)
753
+ .stroke(Color.cyan.opacity(0.28), lineWidth: 0.8)
754
+ )
755
+ .onChange(of: run.id) { _, _ in
756
+ executionExpanded = false
757
+ logsExpanded = false
758
+ }
759
+ }
760
+
761
+ private var detailTitleBar: some View {
762
+ HStack(spacing: 5) {
763
+ Image(systemName: "rectangle.inset.filled.and.person.filled")
764
+ .font(.system(size: 9))
765
+ .foregroundColor(.cyan)
766
+ Text(L("Task Detail", "任务详情"))
767
+ .font(.system(size: 10.5, weight: .bold, design: .rounded))
768
+ .foregroundColor(.white)
769
+ Spacer()
770
+ Text(run.localizedFormattedDate)
771
+ .font(.system(size: 7.4))
772
+ .foregroundColor(.white.opacity(0.38))
773
+ Button(action: onClose) {
774
+ Image(systemName: "xmark.circle.fill")
775
+ .font(.system(size: 12))
776
+ .foregroundColor(.white.opacity(0.48))
777
+ }
778
+ .buttonStyle(.plain)
779
+ .help(L("Close", "关闭"))
780
+ }
781
+ }
782
+
783
+ private var detailScroll: some View {
784
+ ScrollView(.vertical, showsIndicators: true) {
785
+ VStack(spacing: 7) {
786
+ detailHeader
787
+ detailMetrics
788
+ quotaSection
789
+ detailNarrative
790
+ TokenUsageBreakdownView(
791
+ usage: run.aggregatedUsage,
792
+ workerTokens: run.workerTotalTokens,
793
+ parentTokens: run.parentTotalTokens
794
+ )
795
+ skillsSection
796
+ detailWorkers
797
+ stepsSection
798
+ logsSection
799
+ }
800
+ }
801
+ .frame(maxHeight: 360)
802
+ }
803
+
804
+ @ViewBuilder
805
+ private var quotaSection: some View {
806
+ if !run.effectiveQuotaWindows.isEmpty {
807
+ let isPending = run.isRunning && (run.quotaAfter ?? []).isEmpty
808
+ QuotaWindowsView(windows: run.effectiveQuotaWindows, isRunning: isPending)
809
+ }
810
+ }
811
+
812
+ @ViewBuilder
813
+ private var skillsSection: some View {
814
+ if run.hasSkillsOrTools { InspectorSkillsToolsView(run: run) }
815
+ }
816
+
817
+ @ViewBuilder
818
+ private var stepsSection: some View {
819
+ if let steps = run.trajectory, !steps.isEmpty { detailSteps(steps) }
820
+ }
821
+
822
+ @ViewBuilder
823
+ private var logsSection: some View {
824
+ if let logs = run.logs, !logs.isEmpty { detailLogs(logs) }
825
+ }
826
+
827
+ private var detailBackground: some View {
828
+ RoundedRectangle(cornerRadius: 13)
829
+ .fill(Color(red: 0.045, green: 0.052, blue: 0.073).opacity(0.985))
830
+ .shadow(color: .black.opacity(0.55), radius: 16, y: 7)
831
+ }
832
+
833
+ private var detailHeader: some View {
834
+ VStack(alignment: .leading, spacing: 3) {
835
+ HStack(spacing: 4) {
836
+ HoverRevealText(
837
+ run.projectName,
838
+ font: .system(size: 9.6, weight: .bold, design: .rounded),
839
+ foregroundColor: .white.opacity(0.9),
840
+ lineLimit: 1,
841
+ privacyBlur: isPrivacyMode,
842
+ popoverWidth: 300
843
+ )
844
+ if run.isInternalTask {
845
+ Text(L("System", "系统"))
846
+ .font(.system(size: 6.8, weight: .semibold))
847
+ .foregroundColor(.white.opacity(0.55))
848
+ .padding(.horizontal, 3)
849
+ .padding(.vertical, 0.5)
850
+ .background(Color.white.opacity(0.1))
851
+ .clipShape(RoundedRectangle(cornerRadius: 3))
852
+ }
853
+ if run.isAborted {
854
+ Text(L("Interrupted", "已中断"))
855
+ .font(.system(size: 6.8, weight: .semibold))
856
+ .foregroundColor(.orange.opacity(0.9))
857
+ .padding(.horizontal, 3)
858
+ .padding(.vertical, 0.5)
859
+ .background(Color.orange.opacity(0.15))
860
+ .clipShape(RoundedRectangle(cornerRadius: 3))
861
+ }
862
+ if let branch = run.gitBranch, !branch.isEmpty {
863
+ HoverRevealText(
864
+ "(\(branch))",
865
+ font: .system(size: 7.8, design: .monospaced),
866
+ foregroundColor: .cyan.opacity(0.75),
867
+ lineLimit: 1,
868
+ privacyBlur: isPrivacyMode,
869
+ popoverWidth: 300
870
+ )
871
+ }
872
+ Spacer()
873
+ }
874
+
875
+ HoverRevealText(
876
+ run.turnPreview,
877
+ font: .system(size: 8.7),
878
+ foregroundColor: .white.opacity(0.62),
879
+ lineLimit: 2,
880
+ privacyBlur: isPrivacyMode,
881
+ popoverWidth: 390
882
+ )
883
+ .frame(maxWidth: .infinity, alignment: .leading)
884
+ }
885
+ .padding(7)
886
+ .background(detailCard)
887
+ }
888
+
889
+ private var detailMetrics: some View {
890
+ HStack(spacing: 5) {
891
+ miniMetric(L("Time", "耗时"), run.formattedDuration, .cyan)
892
+ miniMetric(L("Tokens", "Token"), run.formattedTotalTokens, Color(red: 0.95, green: 0.35, blue: 0.8))
893
+ if run.workerTotalTokens > 0 {
894
+ miniMetric(L("Worker", "Worker"), "\(run.formattedWorkerTokens) (\(run.formattedWorkerSharePercent))", .teal)
895
+ }
896
+ miniMetric(L("Output", "输出"), TaskRun.formatTokenCount(run.aggregatedUsage.effectiveOutputTokens), .green)
897
+ }
898
+ }
899
+
900
+ private func miniMetric(_ title: String, _ value: String, _ tint: Color) -> some View {
901
+ VStack(spacing: 2) {
902
+ Text(title)
903
+ .font(.system(size: 7.2, weight: .semibold))
904
+ .foregroundColor(.white.opacity(0.42))
905
+ Text(value)
906
+ .font(.system(size: 9.2, weight: .bold, design: .rounded))
907
+ .foregroundColor(tint)
908
+ .lineLimit(1)
909
+ }
910
+ .frame(maxWidth: .infinity)
911
+ .padding(.vertical, 5)
912
+ .background(detailCard)
913
+ }
914
+
915
+ private var detailNarrative: some View {
916
+ VStack(alignment: .leading, spacing: 5) {
917
+ if let goal = run.effectiveGoal, !goal.isEmpty {
918
+ detailTextBlock(L("Objective", "目标"), goal, .cyan, 3)
919
+ }
920
+ if let conclusion = run.effectiveConclusion, !conclusion.isEmpty {
921
+ detailTextBlock(L("Outcome", "结论"), conclusion, .green, 4)
922
+ }
923
+ }
924
+ }
925
+
926
+ private func detailTextBlock(_ title: String, _ text: String, _ tint: Color, _ lines: Int) -> some View {
927
+ VStack(alignment: .leading, spacing: 2) {
928
+ Text(title)
929
+ .font(.system(size: 7.6, weight: .bold))
930
+ .foregroundColor(tint.opacity(0.85))
931
+ HoverRevealText(
932
+ text,
933
+ font: .system(size: 8.5),
934
+ foregroundColor: .white.opacity(0.75),
935
+ lineLimit: lines,
936
+ privacyBlur: isPrivacyMode,
937
+ popoverWidth: 390
938
+ )
939
+ .frame(maxWidth: .infinity, alignment: .leading)
940
+ }
941
+ .padding(7)
942
+ .background(detailCard)
943
+ }
944
+
945
+ private var detailWorkers: some View {
946
+ VStack(alignment: .leading, spacing: 4) {
947
+ HStack {
948
+ Text(L("Participants", "参与 Agent"))
949
+ .font(.system(size: 7.7, weight: .bold))
950
+ .foregroundColor(.white.opacity(0.48))
951
+ Spacer()
952
+ let workerSummary = run.workerTotalTokens > 0
953
+ ? L("\(run.allWorkers.count) workers · \(run.formattedWorkerTokens) (\(run.formattedWorkerSharePercent))",
954
+ "\(run.allWorkers.count) 个 Worker · \(run.formattedWorkerTokens)(\(run.formattedWorkerSharePercent))")
955
+ : L("\(run.allWorkers.count) workers", "\(run.allWorkers.count) 个 Worker")
956
+ Text(workerSummary)
957
+ .font(.system(size: 7.3))
958
+ .foregroundColor(.teal.opacity(0.85))
959
+ }
960
+
961
+ HStack(spacing: 3) {
962
+ Text(L("Parent", "父 Agent"))
963
+ .font(.system(size: 7.4, weight: .semibold))
964
+ .foregroundColor(.indigo)
965
+ HoverRevealText(
966
+ run.parent?.displayModel ?? "unknown",
967
+ font: .system(size: 7.6, weight: .medium, design: .monospaced),
968
+ foregroundColor: .white.opacity(0.7),
969
+ lineLimit: 1,
970
+ popoverWidth: 300
971
+ )
972
+ if let effort = run.parent?.displayEffort {
973
+ Text(effort)
974
+ .font(.system(size: 6.8, weight: .medium, design: .monospaced))
975
+ .foregroundColor(.indigo.opacity(0.9))
976
+ .padding(.horizontal, 3.5)
977
+ .padding(.vertical, 0.5)
978
+ .background(Capsule().fill(Color.indigo.opacity(0.15)))
979
+ }
980
+ Spacer()
981
+ if let tokens = run.parent?.effectiveUsage?.totalTokens, tokens > 0 {
982
+ Text(TaskRun.formatTokenCount(tokens))
983
+ .font(.system(size: 7.3, design: .monospaced))
984
+ .foregroundColor(.white.opacity(0.4))
985
+ }
986
+ }
987
+
988
+ ForEach(run.allWorkers.prefix(4)) { worker in
989
+ HStack(spacing: 3) {
990
+ Text(L("Worker", "Worker"))
991
+ .font(.system(size: 7.4, weight: .semibold))
992
+ .foregroundColor(.teal)
993
+ HoverRevealText(
994
+ worker.name ?? worker.displayModel,
995
+ font: .system(size: 7.6),
996
+ foregroundColor: .white.opacity(0.68),
997
+ lineLimit: 1,
998
+ popoverWidth: 320
999
+ )
1000
+ if let effort = worker.displayEffort {
1001
+ Text(effort)
1002
+ .font(.system(size: 6.8, weight: .medium, design: .monospaced))
1003
+ .foregroundColor(.teal.opacity(0.9))
1004
+ .padding(.horizontal, 3.5)
1005
+ .padding(.vertical, 0.5)
1006
+ .background(Capsule().fill(Color.teal.opacity(0.15)))
1007
+ }
1008
+ Spacer()
1009
+ Text(TaskRun.formatTokenCount(worker.effectiveUsage?.totalTokens ?? 0))
1010
+ .font(.system(size: 7.3, design: .monospaced))
1011
+ .foregroundColor(.white.opacity(0.4))
1012
+ }
1013
+ }
1014
+ }
1015
+ .padding(7)
1016
+ .background(detailCard)
1017
+ }
1018
+
1019
+ private func detailSteps(_ steps: [TrajectoryStep]) -> some View {
1020
+ VStack(alignment: .leading, spacing: 4) {
1021
+ Button {
1022
+ withAnimation(.easeInOut(duration: 0.15)) {
1023
+ executionExpanded.toggle()
1024
+ }
1025
+ } label: {
1026
+ HStack(spacing: 4) {
1027
+ Image(systemName: "point.filled.topleft.down.curvedto.point.bottomright.up")
1028
+ .font(.system(size: 7.5))
1029
+ .foregroundColor(.indigo)
1030
+ Text(L("Execution", "执行轨迹"))
1031
+ .font(.system(size: 7.7, weight: .bold))
1032
+ .foregroundColor(.white.opacity(0.48))
1033
+ Spacer()
1034
+ Text(L("\(steps.count) steps", "\(steps.count) 步"))
1035
+ .font(.system(size: 7.1, weight: .medium))
1036
+ .foregroundColor(.white.opacity(0.32))
1037
+ Image(systemName: executionExpanded ? "chevron.up" : "chevron.down")
1038
+ .font(.system(size: 6.5, weight: .bold))
1039
+ .foregroundColor(.white.opacity(0.32))
1040
+ }
1041
+ .contentShape(Rectangle())
1042
+ }
1043
+ .buttonStyle(.plain)
1044
+
1045
+ if executionExpanded {
1046
+ ForEach(Array(steps.prefix(5))) { step in
1047
+ HStack(alignment: .top, spacing: 4) {
1048
+ Circle()
1049
+ .fill(step.status == "error" ? Color.orange : Color.cyan)
1050
+ .frame(width: 4, height: 4)
1051
+ .padding(.top, 4)
1052
+ HoverRevealText(
1053
+ [step.title, step.detail].compactMap { $0 }.joined(separator: " — "),
1054
+ font: .system(size: 7.6),
1055
+ foregroundColor: .white.opacity(0.6),
1056
+ lineLimit: 1,
1057
+ privacyBlur: isPrivacyMode,
1058
+ popoverWidth: 400
1059
+ )
1060
+ .frame(maxWidth: .infinity, alignment: .leading)
1061
+ }
1062
+ }
1063
+ }
1064
+ }
1065
+ .padding(7)
1066
+ .background(detailCard)
1067
+ }
1068
+
1069
+ private func detailLogs(_ logs: [TaskLogEntry]) -> some View {
1070
+ VStack(alignment: .leading, spacing: 4) {
1071
+ Button {
1072
+ withAnimation(.easeInOut(duration: 0.15)) {
1073
+ logsExpanded.toggle()
1074
+ }
1075
+ } label: {
1076
+ HStack(spacing: 4) {
1077
+ Image(systemName: "text.line.first.and.arrowtriangle.forward")
1078
+ .font(.system(size: 7.5))
1079
+ .foregroundColor(.green)
1080
+ Text(L("Recent logs", "最近日志"))
1081
+ .font(.system(size: 7.7, weight: .bold))
1082
+ .foregroundColor(.white.opacity(0.48))
1083
+ Spacer()
1084
+ Text(L("\(logs.count) entries", "\(logs.count) 条"))
1085
+ .font(.system(size: 7.1, weight: .medium))
1086
+ .foregroundColor(.white.opacity(0.32))
1087
+ Image(systemName: logsExpanded ? "chevron.up" : "chevron.down")
1088
+ .font(.system(size: 6.5, weight: .bold))
1089
+ .foregroundColor(.white.opacity(0.32))
1090
+ }
1091
+ .contentShape(Rectangle())
1092
+ }
1093
+ .buttonStyle(.plain)
1094
+
1095
+ if logsExpanded {
1096
+ ForEach(Array(logs.suffix(4))) { entry in
1097
+ HStack(alignment: .top, spacing: 4) {
1098
+ Circle()
1099
+ .fill(entry.level == "error" ? Color.orange : Color.green)
1100
+ .frame(width: 4, height: 4)
1101
+ .padding(.top, 4)
1102
+ HoverRevealText(
1103
+ entry.message ?? "",
1104
+ font: .system(size: 7.4, design: .monospaced),
1105
+ foregroundColor: entry.level == "error" ? .orange.opacity(0.82) : .white.opacity(0.58),
1106
+ lineLimit: 2,
1107
+ privacyBlur: isPrivacyMode,
1108
+ popoverWidth: 410
1109
+ )
1110
+ .frame(maxWidth: .infinity, alignment: .leading)
1111
+ }
1112
+ }
1113
+ }
1114
+ }
1115
+ .padding(7)
1116
+ .background(detailCard)
1117
+ }
1118
+
1119
+ private var detailCard: some View {
1120
+ RoundedRectangle(cornerRadius: 7)
1121
+ .fill(Color.white.opacity(0.035))
1122
+ .overlay(RoundedRectangle(cornerRadius: 7).stroke(Color.white.opacity(0.065), lineWidth: 0.6))
1123
+ }
1124
+ }