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,1273 @@
1
+ import SwiftUI
2
+ import AppKit
3
+
4
+ /// Main expanded FlowPilot surface. Account remains a UI-only fourth tab so
5
+ /// existing Inspector/History/Analytics IPC commands stay backward compatible.
6
+ public struct SummaryView: View {
7
+ @ObservedObject var state: OverlayState
8
+ @ObservedObject private var localization = AppLocalization.shared
9
+ @ObservedObject private var updateService = FlowPilotUpdateService.shared
10
+ public var isFullHeight: Bool = false
11
+
12
+ @State private var showingAccount = false
13
+ @State private var copiedSummary = false
14
+ @State private var showUpdatePopover = false
15
+ @State private var executionDetailsExpanded = false
16
+
17
+ public init(state: OverlayState, isFullHeight: Bool = false) {
18
+ self.state = state
19
+ self.isFullHeight = isFullHeight
20
+ }
21
+
22
+ private var currentRun: TaskRun? { state.inspectedRun ?? state.latestRun }
23
+
24
+ public var body: some View {
25
+ VStack(spacing: 0) {
26
+ topBar
27
+ Divider().background(Color.white.opacity(0.07))
28
+ tabBar
29
+ Divider().background(Color.white.opacity(0.06))
30
+
31
+ Group {
32
+ if showingAccount {
33
+ AccountView(state: state, isFullHeight: isFullHeight)
34
+ } else {
35
+ switch state.activeTab {
36
+ case .inspector:
37
+ inspectorSurface
38
+ case .history:
39
+ HistoryView(state: state, isFullHeight: isFullHeight)
40
+ case .analytics:
41
+ AnalyticsView(state: state, isFullHeight: isFullHeight)
42
+ }
43
+ }
44
+ }
45
+ .padding(.horizontal, 10)
46
+ .padding(.bottom, 9)
47
+ }
48
+ .background(background)
49
+ .clipShape(RoundedRectangle(cornerRadius: 18, style: .continuous))
50
+ .overlay(
51
+ RoundedRectangle(cornerRadius: 18, style: .continuous)
52
+ .stroke(Color.white.opacity(0.11), lineWidth: 0.8)
53
+ )
54
+ .onChange(of: state.activeTab) { _, _ in
55
+ if showingAccount { showingAccount = false }
56
+ }
57
+ .onChange(of: currentRun?.id) { _, _ in
58
+ executionDetailsExpanded = false
59
+ }
60
+ }
61
+
62
+ // MARK: - Chrome
63
+
64
+ private var topBar: some View {
65
+ HStack(spacing: 7) {
66
+ FlowPilotLogoView(size: 29, showGlow: true, withBolt: false, text: "FP")
67
+ .frame(width: 29, height: 29)
68
+
69
+ VStack(alignment: .leading, spacing: 0) {
70
+ Text("FlowPilot")
71
+ .font(.system(size: 11.5, weight: .heavy, design: .rounded))
72
+ .foregroundColor(.white)
73
+ Text(statusText)
74
+ .font(.system(size: 7.8, weight: .medium, design: .rounded))
75
+ .foregroundColor(statusColor.opacity(0.85))
76
+ }
77
+
78
+ GitHubStarButton()
79
+
80
+ Spacer()
81
+
82
+ Button {
83
+ showUpdatePopover.toggle()
84
+ } label: {
85
+ ZStack(alignment: .topTrailing) {
86
+ Image(systemName: updateService.isRestartRequired ? "arrow.clockwise.circle" : "arrow.down.circle")
87
+ .font(.system(size: 10, weight: .semibold))
88
+ .foregroundColor(updateService.hasUpdateBadge ? .cyan : .white.opacity(0.55))
89
+ .frame(width: 23, height: 23)
90
+ .background(Circle().fill(Color.white.opacity(0.045)))
91
+
92
+ if updateService.hasUpdateBadge {
93
+ Circle()
94
+ .fill(updateService.isRestartRequired ? Color.orange : Color.red)
95
+ .frame(width: 6.5, height: 6.5)
96
+ .overlay(Circle().stroke(Color.black.opacity(0.65), lineWidth: 1))
97
+ .offset(x: 1.5, y: -1.5)
98
+ }
99
+ }
100
+ }
101
+ .buttonStyle(.plain)
102
+ .help(L("Software Update", "软件更新"))
103
+ .popover(isPresented: $showUpdatePopover, arrowEdge: .top) {
104
+ FlowPilotUpdateView()
105
+ }
106
+
107
+ chromeButton(
108
+ state.isPrivacyMode ? "eye.slash.fill" : "eye.fill",
109
+ state.isPrivacyMode ? .orange : .white.opacity(0.55),
110
+ L("Privacy mode", "隐私模式")
111
+ ) { state.isPrivacyMode.toggle() }
112
+
113
+ chromeButton(
114
+ state.isPinned ? "pin.fill" : "pin",
115
+ state.isPinned ? .cyan : .white.opacity(0.55),
116
+ L("Pin window", "置顶窗口")
117
+ ) { state.isPinned.toggle() }
118
+
119
+ chromeButton("chevron.down", .white.opacity(0.55), L("Collapse", "收起")) {
120
+ state.collapse()
121
+ }
122
+ }
123
+ .padding(.horizontal, 11)
124
+ .padding(.vertical, 8)
125
+ }
126
+
127
+ private func chromeButton(_ icon: String, _ tint: Color, _ help: String, action: @escaping () -> Void) -> some View {
128
+ Button(action: action) {
129
+ Image(systemName: icon)
130
+ .font(.system(size: 9, weight: .semibold))
131
+ .foregroundColor(tint)
132
+ .frame(width: 23, height: 23)
133
+ .background(Circle().fill(Color.white.opacity(0.045)))
134
+ }
135
+ .buttonStyle(.plain)
136
+ .help(help)
137
+ }
138
+
139
+ private var tabBar: some View {
140
+ HStack(spacing: 4) {
141
+ ForEach(OverlayTab.allCases) { tab in
142
+ TabButtonView(
143
+ title: tab.localizedTitle,
144
+ icon: tab.iconName,
145
+ selected: !showingAccount && state.activeTab == tab
146
+ ) {
147
+ showingAccount = false
148
+ state.selectTab(tab)
149
+ }
150
+ }
151
+
152
+ TabButtonView(
153
+ title: L("Account", "账户"),
154
+ icon: "person.crop.circle.fill",
155
+ selected: showingAccount
156
+ ) {
157
+ showingAccount = true
158
+ }
159
+ }
160
+ .padding(.horizontal, 6)
161
+ .padding(.vertical, 4)
162
+ .background(Color.black.opacity(0.14))
163
+ }
164
+
165
+ private struct TabButtonView: View {
166
+ let title: String
167
+ let icon: String
168
+ let selected: Bool
169
+ let action: () -> Void
170
+
171
+ @State private var isHovered = false
172
+
173
+ var body: some View {
174
+ Button(action: action) {
175
+ HStack(spacing: 4) {
176
+ Image(systemName: icon)
177
+ .font(.system(size: 8.8, weight: .semibold))
178
+ Text(title)
179
+ .font(.system(size: 9.2, weight: selected ? .bold : .medium, design: .rounded))
180
+ .lineLimit(1)
181
+ .minimumScaleFactor(0.85)
182
+ }
183
+ .foregroundColor(selected ? .white : (isHovered ? .white.opacity(0.85) : .white.opacity(0.52)))
184
+ .frame(maxWidth: .infinity, minHeight: 26)
185
+ .padding(.vertical, 5)
186
+ .background(
187
+ RoundedRectangle(cornerRadius: 7)
188
+ .fill(selected ? Color.cyan.opacity(0.22) : (isHovered ? Color.white.opacity(0.06) : Color.clear))
189
+ .overlay(
190
+ RoundedRectangle(cornerRadius: 7)
191
+ .stroke(selected ? Color.cyan.opacity(0.32) : (isHovered ? Color.white.opacity(0.1) : Color.clear), lineWidth: 0.6)
192
+ )
193
+ )
194
+ .contentShape(Rectangle())
195
+ }
196
+ .buttonStyle(.plain)
197
+ .frame(maxWidth: .infinity)
198
+ .contentShape(Rectangle())
199
+ .onHover { isHovered = $0 }
200
+ }
201
+ }
202
+
203
+ private struct GitHubStarButton: View {
204
+ @State private var isHovered = false
205
+ @State private var hasClicked = false
206
+
207
+ var body: some View {
208
+ Button {
209
+ withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) {
210
+ hasClicked = true
211
+ }
212
+ if let url = URL(string: "https://github.com/ParsifalC/codex-flow") {
213
+ NSWorkspace.shared.open(url)
214
+ }
215
+ DispatchQueue.main.asyncAfter(deadline: .now() + 2.5) {
216
+ withAnimation(.easeInOut(duration: 0.2)) {
217
+ hasClicked = false
218
+ }
219
+ }
220
+ } label: {
221
+ HStack(spacing: 3.5) {
222
+ Image(systemName: hasClicked ? "heart.fill" : "star.fill")
223
+ .font(.system(size: 8.5, weight: .bold))
224
+ .foregroundStyle(
225
+ hasClicked
226
+ ? LinearGradient(
227
+ colors: [Color.pink, Color.red],
228
+ startPoint: .top,
229
+ endPoint: .bottom
230
+ )
231
+ : LinearGradient(
232
+ colors: [
233
+ Color(red: 1.0, green: 0.88, blue: 0.35),
234
+ Color(red: 0.98, green: 0.68, blue: 0.16)
235
+ ],
236
+ startPoint: .top,
237
+ endPoint: .bottom
238
+ )
239
+ )
240
+ .shadow(
241
+ color: (hasClicked ? Color.pink : Color(red: 1.0, green: 0.75, blue: 0.2))
242
+ .opacity(isHovered ? 0.7 : 0.3),
243
+ radius: isHovered ? 3 : 1.5
244
+ )
245
+
246
+ Text(hasClicked ? L("Thanks!", "感谢支持!") : "Star")
247
+ .font(.system(size: 9.2, weight: .bold, design: .rounded))
248
+ .foregroundColor(isHovered ? .white : .white.opacity(0.88))
249
+ }
250
+ .padding(.horizontal, 7)
251
+ .padding(.vertical, 3.5)
252
+ .background(
253
+ Capsule(style: .continuous)
254
+ .fill(
255
+ LinearGradient(
256
+ colors: [
257
+ (hasClicked ? Color.pink : Color(red: 1.0, green: 0.8, blue: 0.2))
258
+ .opacity(isHovered ? 0.18 : 0.08),
259
+ (hasClicked ? Color.purple : Color(red: 1.0, green: 0.55, blue: 0.1))
260
+ .opacity(isHovered ? 0.12 : 0.04)
261
+ ],
262
+ startPoint: .topLeading,
263
+ endPoint: .bottomTrailing
264
+ )
265
+ )
266
+ )
267
+ .overlay(
268
+ Capsule(style: .continuous)
269
+ .stroke(
270
+ LinearGradient(
271
+ colors: [
272
+ (hasClicked ? Color.pink : Color(red: 1.0, green: 0.85, blue: 0.3))
273
+ .opacity(isHovered ? 0.55 : 0.26),
274
+ (hasClicked ? Color.purple : Color(red: 1.0, green: 0.6, blue: 0.15))
275
+ .opacity(isHovered ? 0.32 : 0.12)
276
+ ],
277
+ startPoint: .topLeading,
278
+ endPoint: .bottomTrailing
279
+ ),
280
+ lineWidth: 0.75
281
+ )
282
+ )
283
+ .shadow(
284
+ color: (hasClicked ? Color.pink : Color(red: 1.0, green: 0.75, blue: 0.15))
285
+ .opacity(isHovered ? 0.28 : 0),
286
+ radius: 4,
287
+ y: 1
288
+ )
289
+ .scaleEffect(isHovered ? 1.03 : 1.0)
290
+ }
291
+ .buttonStyle(.plain)
292
+ .help(L("Star FlowPilot on GitHub", "前往 GitHub 为 FlowPilot 点亮 Star"))
293
+ .onHover { isHovered = $0 }
294
+ .animation(.spring(response: 0.2, dampingFraction: 0.75), value: isHovered)
295
+ }
296
+ }
297
+
298
+ private var background: some View {
299
+ RoundedRectangle(cornerRadius: 18, style: .continuous)
300
+ .fill(
301
+ LinearGradient(
302
+ colors: [
303
+ Color(red: 0.055, green: 0.062, blue: 0.085).opacity(0.98),
304
+ Color(red: 0.035, green: 0.041, blue: 0.058).opacity(0.98)
305
+ ],
306
+ startPoint: .topLeading,
307
+ endPoint: .bottomTrailing
308
+ )
309
+ )
310
+ .shadow(color: .black.opacity(0.48), radius: 18, y: 8)
311
+ }
312
+
313
+ // MARK: - Inspector
314
+
315
+ @ViewBuilder
316
+ private var inspectorSurface: some View {
317
+ if let run = currentRun {
318
+ if isFullHeight {
319
+ inspectorContent(run)
320
+ } else {
321
+ ScrollView(.vertical, showsIndicators: true) {
322
+ inspectorContent(run)
323
+ }
324
+ .frame(maxHeight: 440)
325
+ }
326
+ } else {
327
+ idleInspector
328
+ }
329
+ }
330
+
331
+ private func inspectorContent(_ run: TaskRun) -> some View {
332
+ VStack(spacing: 8) {
333
+ if state.inspectedRun != nil { historicalBanner }
334
+ projectHeader(run)
335
+ metricsGrid(run)
336
+
337
+ if !run.effectiveQuotaWindows.isEmpty {
338
+ let isPending = run.isRunning && (run.quotaAfter ?? []).isEmpty
339
+ QuotaWindowsView(windows: run.effectiveQuotaWindows, isRunning: isPending)
340
+ }
341
+
342
+ taskNarrativeCard(run)
343
+ participantsCard(run)
344
+ TokenUsageBreakdownView(
345
+ usage: run.aggregatedUsage,
346
+ workerTokens: run.workerTotalTokens,
347
+ parentTokens: run.parentTotalTokens
348
+ )
349
+
350
+ if run.hasSkillsOrTools {
351
+ InspectorSkillsToolsView(run: run)
352
+ }
353
+
354
+ if !run.allWorkers.isEmpty {
355
+ workerOutcomesCard(run)
356
+ }
357
+
358
+ if run.hasTrajectory || run.hasLogs {
359
+ executionDetailCard(run)
360
+ }
361
+
362
+ actionFooter(run)
363
+ }
364
+ .padding(.top, 8)
365
+ }
366
+
367
+ private var historicalBanner: some View {
368
+ HStack(spacing: 5) {
369
+ Image(systemName: "clock.arrow.circlepath")
370
+ .font(.system(size: 8.5))
371
+ .foregroundColor(.cyan)
372
+ Text(L("Viewing historical task", "正在查看历史任务"))
373
+ .font(.system(size: 8.8, weight: .semibold))
374
+ .foregroundColor(.cyan)
375
+ Spacer()
376
+ recentTaskMenu
377
+ Button(L("Live", "当前任务")) { state.jumpToLive() }
378
+ .buttonStyle(.plain)
379
+ .font(.system(size: 8, weight: .bold))
380
+ .foregroundColor(.white)
381
+ .padding(.horizontal, 6)
382
+ .padding(.vertical, 2)
383
+ .background(Capsule().fill(Color.cyan.opacity(0.3)))
384
+ }
385
+ .padding(7)
386
+ .background(cardBackground(tint: .cyan))
387
+ }
388
+
389
+ private func projectHeader(_ run: TaskRun) -> some View {
390
+ VStack(alignment: .leading, spacing: 4) {
391
+ HStack(spacing: 5) {
392
+ Image(systemName: run.isInternalTask ? "gearshape.fill" : "folder.fill")
393
+ .font(.system(size: 8))
394
+ .foregroundColor(run.isInternalTask ? .cyan.opacity(0.7) : .white.opacity(0.48))
395
+
396
+ HoverRevealText(
397
+ run.projectName,
398
+ font: .system(size: 10, weight: .bold, design: .rounded),
399
+ foregroundColor: .white.opacity(0.94),
400
+ lineLimit: 1,
401
+ privacyBlur: state.isPrivacyMode,
402
+ popoverWidth: 300
403
+ )
404
+
405
+ if run.isInternalTask {
406
+ Text(L("System", "系统"))
407
+ .font(.system(size: 6.8, weight: .semibold))
408
+ .foregroundColor(.white.opacity(0.55))
409
+ .padding(.horizontal, 3)
410
+ .padding(.vertical, 0.5)
411
+ .background(Color.white.opacity(0.1))
412
+ .clipShape(RoundedRectangle(cornerRadius: 3))
413
+ }
414
+
415
+ if let branch = run.gitBranch, !branch.isEmpty {
416
+ Text("·").foregroundColor(.white.opacity(0.25))
417
+ HoverRevealText(
418
+ branch,
419
+ font: .system(size: 8.8, weight: .medium, design: .monospaced),
420
+ foregroundColor: .cyan.opacity(0.88),
421
+ lineLimit: 1,
422
+ privacyBlur: state.isPrivacyMode,
423
+ popoverWidth: 320
424
+ )
425
+ }
426
+
427
+ if !state.isPrivacyMode && !isFullHeight {
428
+ recentTaskMenu
429
+ }
430
+
431
+ Spacer(minLength: 2)
432
+ statusBadge(run)
433
+ }
434
+
435
+ HoverRevealText(
436
+ run.turnPreview,
437
+ font: .system(size: 9.4),
438
+ foregroundColor: .white.opacity(0.63),
439
+ lineLimit: 1,
440
+ privacyBlur: state.isPrivacyMode,
441
+ popoverWidth: 390
442
+ )
443
+ .frame(maxWidth: .infinity, alignment: .leading)
444
+ }
445
+ .padding(8)
446
+ .background(cardBackground())
447
+ }
448
+
449
+ private var recentTaskMenu: some View {
450
+ Menu {
451
+ if let latest = state.latestRun {
452
+ Section(L("Live Session", "当前会话")) {
453
+ Button {
454
+ state.jumpToLive()
455
+ } label: {
456
+ Text(L("⚡ Live · \(latest.projectName) · \(latest.localizedFormattedDate)", "⚡ 当前 · \(latest.projectName) · \(latest.localizedFormattedDate)"))
457
+ }
458
+ }
459
+ }
460
+
461
+ if !state.recentChats.isEmpty {
462
+ Section(L("Recent Chats & Tasks", "最近对话任务")) {
463
+ ForEach(Array(state.recentChats.prefix(10).enumerated()), id: \.element.id) { index, chat in
464
+ if let latest = chat.latestRun {
465
+ Button {
466
+ if latest.id == state.latestRun?.id {
467
+ state.jumpToLive()
468
+ } else {
469
+ state.inspect(run: latest)
470
+ }
471
+ } label: {
472
+ let tag = chat.isInternalTask ? " [\(L("System", "系统"))]" : ""
473
+ Text("#\(index + 1) \(chat.projectName)\(tag) · \(chat.localizedFormattedDate) · \(shortMenuText(chat.title))")
474
+ }
475
+ }
476
+ }
477
+ }
478
+ }
479
+
480
+ Divider()
481
+
482
+ Button {
483
+ state.selectTab(.history)
484
+ } label: {
485
+ Label(L("Browse All History", "浏览全部历史"), systemImage: "clock.arrow.circlepath")
486
+ }
487
+ } label: {
488
+ Image(systemName: "chevron.down.circle.fill")
489
+ .font(.system(size: 9))
490
+ .foregroundColor(.white.opacity(0.42))
491
+ }
492
+ .menuStyle(.borderlessButton)
493
+ .fixedSize()
494
+ .help(L("Switch task", "切换任务"))
495
+ }
496
+
497
+ private func shortMenuText(_ text: String) -> String {
498
+ let cleaned = text.replacingOccurrences(of: "\n", with: " ")
499
+ .trimmingCharacters(in: .whitespacesAndNewlines)
500
+ return cleaned.count > 34 ? String(cleaned.prefix(34)) + "…" : cleaned
501
+ }
502
+
503
+ private func metricsGrid(_ run: TaskRun) -> some View {
504
+ let usage = run.aggregatedUsage
505
+ return HStack(spacing: 6) {
506
+ InspectorMetricView(
507
+ title: L("Time", "耗时"),
508
+ value: run.formattedDuration,
509
+ detail: L("wall time", "任务时长"),
510
+ icon: "timer",
511
+ accent: .cyan,
512
+ progress: min(1, run.durationSeconds / 600.0)
513
+ )
514
+
515
+ InspectorMetricView(
516
+ title: L("Tokens", "Token"),
517
+ value: run.formattedCompactTokens,
518
+ detail: run.workerTotalTokens > 0 ? "W: \(run.formattedWorkerTokens)" : L("total", "总用量"),
519
+ icon: "circle.grid.cross.fill",
520
+ accent: Color(red: 0.95, green: 0.35, blue: 0.8),
521
+ progress: min(1, Double(usage.totalTokens ?? 0) / 100_000.0)
522
+ )
523
+
524
+ // The old Cost ring treated optional credit telemetry like USD and was
525
+ // frequently empty. Output tokens are deterministic and unambiguous.
526
+ InspectorMetricView(
527
+ title: L("Output", "输出"),
528
+ value: TaskRun.formatTokenCount(usage.effectiveOutputTokens),
529
+ detail: L("tokens", "Token"),
530
+ icon: "arrow.up.circle.fill",
531
+ accent: Color(red: 0.25, green: 0.88, blue: 0.58),
532
+ progress: outputShare(usage)
533
+ )
534
+ }
535
+ }
536
+
537
+ private func outputShare(_ usage: TokenUsage) -> Double {
538
+ Double(usage.effectiveOutputTokens) / Double(max(1, usage.totalTokens ?? 0))
539
+ }
540
+
541
+ private func taskNarrativeCard(_ run: TaskRun) -> some View {
542
+ let hasGoal = run.effectiveGoal?.isEmpty == false
543
+ let hasConclusion = run.effectiveConclusion?.isEmpty == false
544
+ return Group {
545
+ if hasGoal || hasConclusion {
546
+ VStack(alignment: .leading, spacing: 6) {
547
+ sectionHeader("sparkles.rectangle.stack.fill", L("Task Objective & Summary", "任务目标与概要说明"), .cyan)
548
+
549
+ if let goal = run.effectiveGoal, !goal.isEmpty {
550
+ narrativeBlock(L("Objective", "目标"), "target", goal, .cyan, 3)
551
+ }
552
+
553
+ if let conclusion = run.effectiveConclusion, !conclusion.isEmpty {
554
+ narrativeBlock(L("Outcome / Conclusion", "交付结论"), "checkmark.seal.fill", conclusion, .green, 4)
555
+ }
556
+ }
557
+ .padding(8)
558
+ .background(cardBackground())
559
+ }
560
+ }
561
+ }
562
+
563
+ private func narrativeBlock(_ label: String, _ icon: String, _ text: String, _ tint: Color, _ lines: Int) -> some View {
564
+ VStack(alignment: .leading, spacing: 3) {
565
+ Label(label, systemImage: icon)
566
+ .font(.system(size: 8.2, weight: .bold))
567
+ .foregroundColor(tint.opacity(0.9))
568
+
569
+ HoverRevealText(
570
+ text,
571
+ font: .system(size: 9.3),
572
+ foregroundColor: .white.opacity(0.88),
573
+ lineLimit: lines,
574
+ privacyBlur: state.isPrivacyMode,
575
+ popoverWidth: 390
576
+ )
577
+ .frame(maxWidth: .infinity, alignment: .leading)
578
+ }
579
+ .padding(6)
580
+ .background(RoundedRectangle(cornerRadius: 7).fill(tint.opacity(0.07)))
581
+ }
582
+
583
+ private func participantsCard(_ run: TaskRun) -> some View {
584
+ HStack(spacing: 6) {
585
+ participantBlock(
586
+ icon: "brain.head.profile",
587
+ title: L("Parent", "父 Agent"),
588
+ name: run.parent?.displayModel ?? L("Direct CLI", "直接 CLI"),
589
+ subtitle: participantSubtitle(run.parent),
590
+ tint: .indigo,
591
+ badge: run.parent?.displayEffort,
592
+ fullText: parentHoverText(run)
593
+ )
594
+
595
+ participantBlock(
596
+ icon: "person.2.fill",
597
+ title: L("Workers", "Worker"),
598
+ name: workerSummaryText(run),
599
+ subtitle: workersSubtitle(run),
600
+ tint: .teal,
601
+ badge: run.allWorkers.isEmpty ? nil : "\(run.allWorkers.count)",
602
+ fullText: workerHoverText(run)
603
+ )
604
+ }
605
+ }
606
+
607
+ private func workerSummaryText(_ run: TaskRun) -> String {
608
+ if run.allWorkers.isEmpty { return L("Direct execution", "直接执行") }
609
+ if run.allWorkers.count == 1 {
610
+ let w = run.allWorkers[0]
611
+ if let name = w.name, name != "default", !name.isEmpty, name != w.displayModel {
612
+ return "\(name) (\(w.displayModel))"
613
+ }
614
+ return w.displayModel
615
+ }
616
+
617
+ var modelCounts: [String: Int] = [:]
618
+ var order: [String] = []
619
+ for w in run.allWorkers {
620
+ let m = w.displayModel
621
+ if modelCounts[m] == nil {
622
+ order.append(m)
623
+ }
624
+ modelCounts[m, default: 0] += 1
625
+ }
626
+
627
+ if order.count == 1 {
628
+ return "\(run.allWorkers.count) × \(order[0])"
629
+ } else {
630
+ return order.map { "\(modelCounts[$0]!) × \($0)" }.joined(separator: ", ")
631
+ }
632
+ }
633
+
634
+ private func workersSubtitle(_ run: TaskRun) -> String {
635
+ if run.allWorkers.isEmpty { return L("No subagents", "无子 Agent") }
636
+ let tokens = run.allWorkers.reduce(0) { $0 + ($1.effectiveUsage?.totalTokens ?? 0) }
637
+ let efforts = Array(Set(run.allWorkers.compactMap { $0.displayEffort }))
638
+ let effortPart = efforts.isEmpty ? nil : efforts.joined(separator: "/")
639
+
640
+ if let effort = effortPart {
641
+ if tokens > 0 {
642
+ return "\(effort) · \(TaskRun.formatTokenCount(tokens)) tokens"
643
+ }
644
+ return effort
645
+ }
646
+ if tokens > 0 {
647
+ return "\(TaskRun.formatTokenCount(tokens)) tokens"
648
+ }
649
+ return L("\(run.allWorkers.count) workers", "\(run.allWorkers.count) 个 Worker")
650
+ }
651
+
652
+ private func parentHoverText(_ run: TaskRun) -> String {
653
+ guard let parent = run.parent else {
654
+ return L("Direct CLI execution (no parent agent metadata)", "直接 CLI 执行(无父 Agent 元数据)")
655
+ }
656
+ var lines: [String] = []
657
+ let name = parent.name ?? L("Parent Agent", "父 Agent")
658
+ let effort = parent.displayEffort.map { " (\($0))" } ?? ""
659
+ lines.append("\(name) · \(parent.displayModel)\(effort)")
660
+ if let tokens = parent.effectiveUsage?.totalTokens, tokens > 0 {
661
+ lines.append(L("Usage: \(TaskRun.formatTokenCount(tokens)) tokens", "用量:\(TaskRun.formatTokenCount(tokens)) Token"))
662
+ }
663
+ return lines.joined(separator: "\n")
664
+ }
665
+
666
+ private func workerHoverText(_ run: TaskRun) -> String {
667
+ if run.allWorkers.isEmpty { return L("Direct execution (no subagents)", "直接执行(无子 Agent)") }
668
+ return run.allWorkers.enumerated().map { index, w in
669
+ let name = w.agentType ?? w.name ?? L("Worker", "Worker")
670
+ let effort = w.displayEffort.map { " (\($0))" } ?? ""
671
+ let tokens = w.effectiveUsage?.totalTokens.map { " · \(TaskRun.formatTokenCount($0)) tokens" } ?? ""
672
+ return "#\(index + 1) \(name) · \(w.displayModel)\(effort)\(tokens)"
673
+ }.joined(separator: "\n")
674
+ }
675
+
676
+ private func participantBlock(
677
+ icon: String,
678
+ title: String,
679
+ name: String,
680
+ subtitle: String,
681
+ tint: Color,
682
+ badge: String? = nil,
683
+ fullText: String? = nil
684
+ ) -> some View {
685
+ VStack(alignment: .leading, spacing: 3) {
686
+ HStack(spacing: 3) {
687
+ Label(title, systemImage: icon)
688
+ .font(.system(size: 8.5, weight: .semibold))
689
+ .foregroundColor(tint.opacity(0.9))
690
+ Spacer()
691
+ if let badge {
692
+ Text(badge)
693
+ .font(.system(size: 6.8, weight: .semibold, design: .monospaced))
694
+ .foregroundColor(tint.opacity(0.9))
695
+ .padding(.horizontal, 3.5)
696
+ .padding(.vertical, 0.5)
697
+ .background(Capsule().fill(tint.opacity(0.14)))
698
+ }
699
+ }
700
+
701
+ HoverRevealText(
702
+ name,
703
+ font: .system(size: 9.8, weight: .bold, design: .rounded),
704
+ foregroundColor: .white.opacity(0.9),
705
+ lineLimit: 1,
706
+ popoverWidth: 280,
707
+ fullText: fullText
708
+ )
709
+
710
+ Text(subtitle)
711
+ .font(.system(size: 7.8))
712
+ .foregroundColor(.white.opacity(0.4))
713
+ .lineLimit(1)
714
+ }
715
+ .padding(7)
716
+ .frame(maxWidth: .infinity, alignment: .leading)
717
+ .background(cardBackground())
718
+ }
719
+
720
+ private func participantSubtitle(_ participant: ParticipantInfo?) -> String {
721
+ guard let participant else { return L("No usage data", "暂无用量数据") }
722
+ let tokens = participant.effectiveUsage?.totalTokens ?? 0
723
+ if let effort = participant.displayEffort {
724
+ if tokens > 0 {
725
+ return "\(effort) · \(TaskRun.formatTokenCount(tokens)) tokens"
726
+ }
727
+ return effort
728
+ }
729
+ if tokens > 0 {
730
+ return "\(TaskRun.formatTokenCount(tokens)) tokens"
731
+ }
732
+ return L("No usage data", "暂无用量数据")
733
+ }
734
+
735
+ private func workerOutcomesCard(_ run: TaskRun) -> some View {
736
+ VStack(alignment: .leading, spacing: 5) {
737
+ sectionHeader("checkmark.bubble.fill", L("Worker Outcomes", "Worker 执行结果"), .teal)
738
+
739
+ ForEach(run.allWorkers) { worker in
740
+ VStack(alignment: .leading, spacing: 2) {
741
+ HStack(spacing: 4) {
742
+ HoverRevealText(
743
+ worker.agentType ?? worker.name ?? L("Worker", "Worker"),
744
+ font: .system(size: 8.7, weight: .semibold, design: .rounded),
745
+ foregroundColor: .white.opacity(0.88),
746
+ lineLimit: 1,
747
+ popoverWidth: 300
748
+ )
749
+ Spacer(minLength: 4)
750
+ HStack(spacing: 3) {
751
+ HoverRevealText(
752
+ worker.displayModel,
753
+ font: .system(size: 7.7, weight: .medium, design: .monospaced),
754
+ foregroundColor: .teal.opacity(0.78),
755
+ lineLimit: 1,
756
+ popoverWidth: 280
757
+ )
758
+ if let effort = worker.displayEffort {
759
+ Text(effort)
760
+ .font(.system(size: 6.8, weight: .medium, design: .monospaced))
761
+ .foregroundColor(.teal.opacity(0.9))
762
+ .padding(.horizontal, 3.5)
763
+ .padding(.vertical, 0.5)
764
+ .background(Capsule().fill(Color.teal.opacity(0.14)))
765
+ }
766
+ }
767
+ }
768
+
769
+ if let conclusion = worker.conclusion, !conclusion.isEmpty {
770
+ HoverRevealText(
771
+ conclusion,
772
+ font: .system(size: 8.8),
773
+ foregroundColor: .white.opacity(0.64),
774
+ lineLimit: 3,
775
+ privacyBlur: state.isPrivacyMode,
776
+ popoverWidth: 390
777
+ )
778
+ .frame(maxWidth: .infinity, alignment: .leading)
779
+ }
780
+ }
781
+ .padding(6)
782
+ .background(RoundedRectangle(cornerRadius: 7).fill(Color.white.opacity(0.025)))
783
+ }
784
+ }
785
+ .padding(8)
786
+ .background(cardBackground())
787
+ }
788
+
789
+ private func executionDetailCard(_ run: TaskRun) -> some View {
790
+ let stepCount = run.trajectory?.count ?? 0
791
+ let logCount = run.logs?.count ?? 0
792
+ return VStack(alignment: .leading, spacing: 5) {
793
+ Button {
794
+ withAnimation(.easeInOut(duration: 0.15)) {
795
+ executionDetailsExpanded.toggle()
796
+ }
797
+ } label: {
798
+ HStack(spacing: 4) {
799
+ Image(systemName: "point.filled.topleft.down.curvedto.point.bottomright.up")
800
+ .font(.system(size: 8))
801
+ .foregroundColor(.indigo)
802
+ Text(L("Execution Details", "执行详情"))
803
+ .font(.system(size: 8.8, weight: .bold, design: .rounded))
804
+ .foregroundColor(.white.opacity(0.7))
805
+ Spacer()
806
+ Text(L("\(stepCount) steps · \(logCount) logs", "\(stepCount) 步 · \(logCount) 条日志"))
807
+ .font(.system(size: 7.3, weight: .medium))
808
+ .foregroundColor(.white.opacity(0.38))
809
+ Image(systemName: executionDetailsExpanded ? "chevron.up" : "chevron.down")
810
+ .font(.system(size: 7, weight: .bold))
811
+ .foregroundColor(.white.opacity(0.35))
812
+ }
813
+ .contentShape(Rectangle())
814
+ }
815
+ .buttonStyle(.plain)
816
+
817
+ if executionDetailsExpanded {
818
+ ForEach(Array((run.trajectory ?? []).prefix(6))) { step in
819
+ HStack(alignment: .top, spacing: 5) {
820
+ Circle()
821
+ .fill(step.status == "error" ? Color.orange : Color.cyan)
822
+ .frame(width: 4.5, height: 4.5)
823
+ .padding(.top, 4)
824
+
825
+ VStack(alignment: .leading, spacing: 1) {
826
+ HoverRevealText(
827
+ step.title ?? step.name ?? "Step",
828
+ font: .system(size: 8.6, weight: .semibold),
829
+ foregroundColor: .white.opacity(0.82),
830
+ lineLimit: 1,
831
+ popoverWidth: 360
832
+ )
833
+
834
+ if let detail = step.detail, !detail.isEmpty {
835
+ HoverRevealText(
836
+ detail,
837
+ font: .system(size: 7.8, design: .monospaced),
838
+ foregroundColor: .white.opacity(0.5),
839
+ lineLimit: 1,
840
+ privacyBlur: state.isPrivacyMode,
841
+ popoverWidth: 410
842
+ )
843
+ }
844
+ }
845
+ }
846
+ }
847
+
848
+ ForEach(Array((run.logs ?? []).suffix(4))) { entry in
849
+ HStack(alignment: .top, spacing: 5) {
850
+ Circle()
851
+ .fill(entry.level == "error" ? Color.orange : Color.green)
852
+ .frame(width: 4, height: 4)
853
+ .padding(.top, 4)
854
+
855
+ HoverRevealText(
856
+ entry.message ?? "",
857
+ font: .system(size: 7.7, design: .monospaced),
858
+ foregroundColor: entry.level == "error" ? .orange.opacity(0.85) : .white.opacity(0.58),
859
+ lineLimit: 2,
860
+ privacyBlur: state.isPrivacyMode,
861
+ popoverWidth: 420
862
+ )
863
+ .frame(maxWidth: .infinity, alignment: .leading)
864
+ }
865
+ }
866
+ }
867
+ }
868
+ .padding(8)
869
+ .background(cardBackground())
870
+ }
871
+
872
+ private func actionFooter(_ run: TaskRun) -> some View {
873
+ HStack(spacing: 6) {
874
+ Button {
875
+ copySummary(run)
876
+ } label: {
877
+ Label(
878
+ copiedSummary ? L("Copied", "已复制") : L("Copy summary", "复制摘要"),
879
+ systemImage: copiedSummary ? "checkmark" : "doc.on.doc"
880
+ )
881
+ .font(.system(size: 8.6, weight: .semibold))
882
+ .foregroundColor(copiedSummary ? .green : .white.opacity(0.75))
883
+ .padding(.horizontal, 7)
884
+ .padding(.vertical, 4)
885
+ .background(RoundedRectangle(cornerRadius: 6).fill(Color.white.opacity(0.05)))
886
+ }
887
+ .buttonStyle(.plain)
888
+
889
+ Button {
890
+ openConsole()
891
+ } label: {
892
+ Label(L("Console", "控制台"), systemImage: "terminal.fill")
893
+ .font(.system(size: 8.6, weight: .semibold))
894
+ .foregroundColor(.white.opacity(0.75))
895
+ .padding(.horizontal, 7)
896
+ .padding(.vertical, 4)
897
+ .background(RoundedRectangle(cornerRadius: 6).fill(Color.white.opacity(0.05)))
898
+ }
899
+ .buttonStyle(.plain)
900
+
901
+ Spacer()
902
+ Text(run.localizedFormattedDate)
903
+ .font(.system(size: 7.8))
904
+ .foregroundColor(.white.opacity(0.35))
905
+ }
906
+ }
907
+
908
+ private var idleInspector: some View {
909
+ VStack(spacing: 10) {
910
+ FlowPilotLogoView(size: 58, showGlow: true, withBolt: true, text: "FlowPilot")
911
+ .frame(width: 58, height: 58)
912
+ .padding(.top, 20)
913
+
914
+ Text(L("FlowPilot is Ready", "FlowPilot 已就绪"))
915
+ .font(.system(size: 13, weight: .bold, design: .rounded))
916
+ .foregroundColor(.white)
917
+
918
+ Text(L("Listening for live task and usage telemetry", "正在监听任务与用量遥测"))
919
+ .font(.system(size: 9.5))
920
+ .foregroundColor(.white.opacity(0.48))
921
+
922
+ HStack(spacing: 7) {
923
+ Button { showingAccount = true } label: {
924
+ Label(L("Account Limits", "账户额度"), systemImage: "person.crop.circle")
925
+ .font(.system(size: 9, weight: .semibold))
926
+ .foregroundColor(.cyan)
927
+ .padding(.horizontal, 9)
928
+ .padding(.vertical, 5)
929
+ .background(Capsule().fill(Color.cyan.opacity(0.12)))
930
+ }
931
+ .buttonStyle(.plain)
932
+
933
+ Button { openConsole() } label: {
934
+ Label(L("Console", "控制台"), systemImage: "terminal.fill")
935
+ .font(.system(size: 9, weight: .semibold))
936
+ .foregroundColor(.white.opacity(0.72))
937
+ .padding(.horizontal, 9)
938
+ .padding(.vertical, 5)
939
+ .background(Capsule().fill(Color.white.opacity(0.06)))
940
+ }
941
+ .buttonStyle(.plain)
942
+ }
943
+
944
+ Spacer(minLength: 12)
945
+ }
946
+ .frame(maxWidth: .infinity, minHeight: 300)
947
+ }
948
+
949
+ // MARK: - Helpers
950
+
951
+ private func statusBadge(_ run: TaskRun) -> some View {
952
+ HStack(spacing: 3) {
953
+ Circle().fill(runStatusColor(run)).frame(width: 4.5, height: 4.5)
954
+ Text(runStatusText(run))
955
+ .font(.system(size: 7.4, weight: .bold, design: .rounded))
956
+ }
957
+ .foregroundColor(runStatusColor(run))
958
+ .padding(.horizontal, 5)
959
+ .padding(.vertical, 2)
960
+ .background(Capsule().fill(runStatusColor(run).opacity(0.12)))
961
+ }
962
+
963
+ private func runStatusColor(_ run: TaskRun) -> Color {
964
+ run.isRunning ? .cyan : (run.isError ? .orange : .green)
965
+ }
966
+
967
+ private func runStatusText(_ run: TaskRun) -> String {
968
+ run.isRunning
969
+ ? L("RUNNING", "运行中")
970
+ : (run.isError ? L("FAILED", "失败") : L("COMPLETED", "已完成"))
971
+ }
972
+
973
+ private var statusText: String { currentRun.map(runStatusText) ?? L("READY", "就绪") }
974
+ private var statusColor: Color { currentRun.map(runStatusColor) ?? .green }
975
+
976
+ private func sectionHeader(_ icon: String, _ title: String, _ tint: Color) -> some View {
977
+ HStack(spacing: 4) {
978
+ Image(systemName: icon)
979
+ .font(.system(size: 8))
980
+ .foregroundColor(tint)
981
+ Text(title)
982
+ .font(.system(size: 8.8, weight: .bold, design: .rounded))
983
+ .foregroundColor(.white.opacity(0.7))
984
+ Spacer()
985
+ }
986
+ }
987
+
988
+ private func cardBackground(tint: Color? = nil) -> some View {
989
+ let base = tint ?? Color.white
990
+ return RoundedRectangle(cornerRadius: 9)
991
+ .fill(base.opacity(tint == nil ? 0.04 : 0.07))
992
+ .overlay(
993
+ RoundedRectangle(cornerRadius: 9)
994
+ .stroke(base.opacity(tint == nil ? 0.075 : 0.18), lineWidth: 0.7)
995
+ )
996
+ }
997
+
998
+ private func copySummary(_ run: TaskRun) {
999
+ let usage = run.aggregatedUsage
1000
+ let text = L(
1001
+ "FlowPilot Task Summary\nProject: \(run.projectName)\nDuration: \(run.formattedDuration)\nTotal tokens: \(run.formattedTotalTokens)\nInput tokens: \(TaskRun.formatTokenCount(usage.effectivePromptTokens))\nOutput tokens: \(TaskRun.formatTokenCount(usage.effectiveOutputTokens))\nWorkers: \(run.allWorkers.count)\nSummary: \(run.turnPreview)",
1002
+ "FlowPilot 任务摘要\n项目:\(run.projectName)\n耗时:\(run.formattedDuration)\n总 Token:\(run.formattedTotalTokens)\n输入 Token:\(TaskRun.formatTokenCount(usage.effectivePromptTokens))\n输出 Token:\(TaskRun.formatTokenCount(usage.effectiveOutputTokens))\nWorker:\(run.allWorkers.count)\n摘要:\(run.turnPreview)"
1003
+ )
1004
+ NSPasteboard.general.clearContents()
1005
+ NSPasteboard.general.setString(text, forType: .string)
1006
+ copiedSummary = true
1007
+ DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
1008
+ copiedSummary = false
1009
+ }
1010
+ }
1011
+
1012
+ private func openConsole() {
1013
+ let script = """
1014
+ tell application "Terminal"
1015
+ activate
1016
+ do script "codex-flow"
1017
+ end tell
1018
+ """
1019
+ if let appleScript = NSAppleScript(source: script) {
1020
+ var error: NSDictionary?
1021
+ appleScript.executeAndReturnError(&error)
1022
+ }
1023
+ }
1024
+ }
1025
+
1026
+ // MARK: - Inspector Metric
1027
+
1028
+ public struct InspectorMetricView: View {
1029
+ public let title: String
1030
+ public let value: String
1031
+ public let detail: String
1032
+ public let icon: String
1033
+ public let accent: Color
1034
+ public let progress: Double
1035
+
1036
+ public var body: some View {
1037
+ VStack(spacing: 3) {
1038
+ ZStack {
1039
+ Circle().stroke(Color.white.opacity(0.07), lineWidth: 3)
1040
+ Circle()
1041
+ .trim(from: 0, to: CGFloat(max(0.015, min(1, progress))))
1042
+ .stroke(accent, style: StrokeStyle(lineWidth: 3, lineCap: .round))
1043
+ .rotationEffect(.degrees(-90))
1044
+ Image(systemName: icon)
1045
+ .font(.system(size: 8.5, weight: .semibold))
1046
+ .foregroundColor(accent)
1047
+ }
1048
+ .frame(width: 29, height: 29)
1049
+
1050
+ Text(title)
1051
+ .font(.system(size: 7.6, weight: .semibold, design: .rounded))
1052
+ .foregroundColor(.white.opacity(0.48))
1053
+ .textCase(.uppercase)
1054
+ Text(value)
1055
+ .font(.system(size: 10.5, weight: .bold, design: .rounded))
1056
+ .foregroundColor(.white)
1057
+ .lineLimit(1)
1058
+ Text(detail)
1059
+ .font(.system(size: 6.9))
1060
+ .foregroundColor(.white.opacity(0.32))
1061
+ .lineLimit(1)
1062
+ }
1063
+ .frame(maxWidth: .infinity)
1064
+ .padding(.vertical, 7)
1065
+ .background(
1066
+ RoundedRectangle(cornerRadius: 9)
1067
+ .fill(Color.white.opacity(0.035))
1068
+ .overlay(RoundedRectangle(cornerRadius: 9).stroke(Color.white.opacity(0.07), lineWidth: 0.7))
1069
+ )
1070
+ }
1071
+ }
1072
+
1073
+ // MARK: - Quota Windows
1074
+
1075
+ public struct QuotaWindowsView: View {
1076
+ public let windows: [QuotaWindow]
1077
+ public let isRunning: Bool
1078
+
1079
+ public init(windows: [QuotaWindow], isRunning: Bool = false) {
1080
+ self.windows = windows
1081
+ self.isRunning = isRunning
1082
+ }
1083
+
1084
+ private var ordered: [QuotaWindow] {
1085
+ windows.sorted { ($0.windowDurationMins ?? Int.max) < ($1.windowDurationMins ?? Int.max) }
1086
+ }
1087
+
1088
+ private var cardTitle: String {
1089
+ isRunning
1090
+ ? L("Quota remaining at task start", "任务起始额度剩余")
1091
+ : L("Quota remaining at task completion", "任务结束额度剩余")
1092
+ }
1093
+
1094
+ public var body: some View {
1095
+ VStack(alignment: .leading, spacing: 6) {
1096
+ HStack {
1097
+ Label(cardTitle, systemImage: "gauge.with.needle.fill")
1098
+ .font(.system(size: 8.8, weight: .bold, design: .rounded))
1099
+ .foregroundColor(.white.opacity(0.7))
1100
+
1101
+ Spacer()
1102
+
1103
+ if isRunning {
1104
+ HStack(spacing: 3) {
1105
+ Circle()
1106
+ .fill(Color.cyan)
1107
+ .frame(width: 4, height: 4)
1108
+ Text(L("Running · Settles upon completion", "运行中 · 结束后结算消耗"))
1109
+ .font(.system(size: 7.2, weight: .medium))
1110
+ .foregroundColor(.cyan.opacity(0.85))
1111
+ }
1112
+ }
1113
+ }
1114
+
1115
+ ForEach(ordered) { quotaRow($0) }
1116
+ }
1117
+ .padding(8)
1118
+ .background(
1119
+ RoundedRectangle(cornerRadius: 9)
1120
+ .fill(Color.white.opacity(0.035))
1121
+ .overlay(RoundedRectangle(cornerRadius: 9).stroke(Color.white.opacity(0.07), lineWidth: 0.7))
1122
+ )
1123
+ }
1124
+
1125
+ private func quotaRow(_ window: QuotaWindow) -> some View {
1126
+ let used = max(0, min(100, window.usedPercent ?? 0))
1127
+ let remaining = window.remainingPercent
1128
+ let tint: Color = used >= 85 ? .orange : (used >= 60 ? .yellow : .cyan)
1129
+
1130
+ return VStack(spacing: 3) {
1131
+ HStack(alignment: .firstTextBaseline, spacing: 4) {
1132
+ Text(window.label)
1133
+ .font(.system(size: 8, weight: .heavy, design: .monospaced))
1134
+ .foregroundColor(tint)
1135
+ .frame(width: 34, alignment: .leading)
1136
+
1137
+ Text(String(format: L("%.0f%% left", "剩余 %.0f%%"), remaining))
1138
+ .font(.system(size: 8, weight: .bold, design: .rounded))
1139
+ .foregroundColor(.white.opacity(0.72))
1140
+
1141
+ if let delta = window.deltaPercentagePoints, abs(delta) >= 0.1 {
1142
+ let deltaText = abs(delta) < 0.95
1143
+ ? String(format: "%.1f%%", abs(delta))
1144
+ : String(format: "%.0f%%", abs(delta))
1145
+ Text(delta > 0
1146
+ ? String(format: L("(%@ used)", "(消耗 %@)"), deltaText)
1147
+ : String(format: L("(%@ restored)", "(恢复 %@)"), deltaText)
1148
+ )
1149
+ .font(.system(size: 7.6, weight: .bold, design: .rounded))
1150
+ .foregroundColor(delta > 0 ? .orange : .green)
1151
+ .help(delta > 0
1152
+ ? String(format: L("Quota consumed during this run: %@", "本轮任务配额消耗:%@"), deltaText)
1153
+ : String(format: L("Quota restored during this run: %@", "本轮任务配额恢复:%@"), deltaText)
1154
+ )
1155
+ }
1156
+
1157
+ Spacer(minLength: 2)
1158
+
1159
+ if let reset = window.localizedFormattedResetsAt {
1160
+ Text(reset)
1161
+ .font(.system(size: 7.3, weight: .medium, design: .monospaced))
1162
+ .foregroundColor(.white.opacity(0.42))
1163
+ .lineLimit(1)
1164
+ .minimumScaleFactor(0.62)
1165
+ .allowsTightening(true)
1166
+ .truncationMode(.tail)
1167
+ .frame(maxWidth: 132, alignment: .trailing)
1168
+ }
1169
+ }
1170
+
1171
+ GeometryReader { proxy in
1172
+ ZStack(alignment: .leading) {
1173
+ Capsule().fill(Color.white.opacity(0.075))
1174
+ Capsule()
1175
+ .fill(tint)
1176
+ .frame(width: proxy.size.width * CGFloat(max(0, min(1, remaining / 100.0))))
1177
+ }
1178
+ }
1179
+ .frame(height: 4)
1180
+ }
1181
+ }
1182
+ }
1183
+
1184
+ // MARK: - Token Usage
1185
+
1186
+ public struct TokenUsageBreakdownView: View {
1187
+ public let usage: TokenUsage
1188
+ public let workerTokens: Int?
1189
+ public let parentTokens: Int?
1190
+
1191
+ public init(usage: TokenUsage, workerTokens: Int? = nil, parentTokens: Int? = nil) {
1192
+ self.usage = usage
1193
+ self.workerTokens = workerTokens
1194
+ self.parentTokens = parentTokens
1195
+ }
1196
+
1197
+ public var body: some View {
1198
+ // `inputTokens` includes cached input and `outputTokens` can include
1199
+ // reasoning output. Make the rendered segments mutually exclusive so
1200
+ // the bar and legend do not double-count those subsets.
1201
+ let input = usage.effectivePromptTokens
1202
+ let output = usage.effectiveOutputTokens
1203
+ let cached = min(max(0, usage.effectiveCachedTokens), max(0, input))
1204
+ let reasoning = min(max(0, usage.effectiveReasoningTokens), max(0, output))
1205
+ let newInput = max(0, input - cached)
1206
+ let visibleOutput = max(0, output - reasoning)
1207
+ let segmentedTotal = newInput + cached + visibleOutput + reasoning
1208
+ let total = max(1, segmentedTotal)
1209
+ let reportedTotal = usage.totalTokens ?? segmentedTotal
1210
+
1211
+ return VStack(alignment: .leading, spacing: 5) {
1212
+ HStack(spacing: 4) {
1213
+ Text(L("Token usage", "Token 用量"))
1214
+ .font(.system(size: 8.8, weight: .bold, design: .rounded))
1215
+ .foregroundColor(.white.opacity(0.7))
1216
+ if let wTok = workerTokens, wTok > 0 {
1217
+ let share = Double(wTok) / Double(max(1, reportedTotal)) * 100.0
1218
+ Text(L("Worker: \(TaskRun.formatTokenCount(wTok)) (\(String(format: "%.1f%%", share)))", "Worker: \(TaskRun.formatTokenCount(wTok)) (\(String(format: "%.1f%%", share)))"))
1219
+ .font(.system(size: 7.2, weight: .semibold, design: .rounded))
1220
+ .foregroundColor(.teal.opacity(0.9))
1221
+ .padding(.horizontal, 4)
1222
+ .padding(.vertical, 1)
1223
+ .background(Capsule().fill(Color.teal.opacity(0.12)))
1224
+ }
1225
+ Spacer()
1226
+ Text(TaskRun.formatTokenCount(reportedTotal))
1227
+ .font(.system(size: 8.5, weight: .bold, design: .rounded))
1228
+ .foregroundColor(Color(red: 0.95, green: 0.35, blue: 0.8))
1229
+ }
1230
+
1231
+ GeometryReader { proxy in
1232
+ HStack(spacing: 1) {
1233
+ segment(proxy.size.width, newInput, total, .cyan)
1234
+ segment(proxy.size.width, cached, total, .indigo)
1235
+ segment(proxy.size.width, visibleOutput, total, .green)
1236
+ segment(proxy.size.width, reasoning, total, .purple)
1237
+ }
1238
+ }
1239
+ .frame(height: 5)
1240
+ .clipShape(Capsule())
1241
+
1242
+ HStack(spacing: 8) {
1243
+ legend(L("New input", "新输入"), newInput, .cyan)
1244
+ if cached > 0 {
1245
+ legend(L("Cached", "缓存"), cached, .indigo)
1246
+ }
1247
+ legend(L("Output", "输出"), visibleOutput, .green)
1248
+ if reasoning > 0 {
1249
+ legend(L("Reasoning", "推理"), reasoning, .purple)
1250
+ }
1251
+ }
1252
+ }
1253
+ .padding(8)
1254
+ .background(
1255
+ RoundedRectangle(cornerRadius: 9)
1256
+ .fill(Color.white.opacity(0.035))
1257
+ .overlay(RoundedRectangle(cornerRadius: 9).stroke(Color.white.opacity(0.07), lineWidth: 0.7))
1258
+ )
1259
+ }
1260
+
1261
+ private func segment(_ width: CGFloat, _ value: Int, _ total: Int, _ color: Color) -> some View {
1262
+ color.frame(width: max(0, width * CGFloat(Double(value) / Double(total))))
1263
+ }
1264
+
1265
+ private func legend(_ title: String, _ value: Int, _ color: Color) -> some View {
1266
+ HStack(spacing: 2.5) {
1267
+ Circle().fill(color).frame(width: 4, height: 4)
1268
+ Text("\(title) \(TaskRun.formatTokenCount(value))")
1269
+ .font(.system(size: 7.1, weight: .medium, design: .rounded))
1270
+ .foregroundColor(.white.opacity(0.45))
1271
+ }
1272
+ }
1273
+ }