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,561 @@
1
+ import SwiftUI
2
+ import Foundation
3
+
4
+ public struct StrategyProfileInfo: Identifiable, Equatable {
5
+ public let name: String
6
+ public let description: String
7
+
8
+ public var id: String { name }
9
+
10
+ public var localizedName: String {
11
+ switch name {
12
+ case "efficient": return L("Efficient", "高效")
13
+ case "balanced": return L("Balanced", "均衡")
14
+ case "quality": return L("Quality", "质量")
15
+ case "speed": return L("Speed", "速度")
16
+ default: return name.capitalized
17
+ }
18
+ }
19
+
20
+ public var localizedDescription: String {
21
+ switch name {
22
+ case "efficient": return L("Minimize quota waste and expensive parent usage.", "优先降低额度浪费和高成本父 Agent 使用。")
23
+ case "balanced": return L("Balance quality, quota, and latency.", "在质量、额度与延迟之间取得平衡。")
24
+ case "quality": return L("Maximize correctness and independent verification.", "优先正确性、深度推理与独立验证。")
25
+ case "speed": return L("Minimize wall-clock time with safe parallelism.", "在安全并行边界内优先降低整体耗时。")
26
+ default: return description
27
+ }
28
+ }
29
+
30
+ public var iconName: String {
31
+ switch name {
32
+ case "efficient": return "leaf.fill"
33
+ case "balanced": return "scale.3d"
34
+ case "quality": return "checkmark.seal.fill"
35
+ case "speed": return "bolt.fill"
36
+ default: return "slider.horizontal.3"
37
+ }
38
+ }
39
+
40
+ public var accent: Color {
41
+ switch name {
42
+ case "efficient": return .green
43
+ case "balanced": return .cyan
44
+ case "quality": return .purple
45
+ case "speed": return .orange
46
+ default: return .cyan
47
+ }
48
+ }
49
+ }
50
+
51
+ public struct StrategyModeSnapshot {
52
+ public let enabled: Bool
53
+ public let configured: String
54
+ public let routing: String?
55
+ public let valid: Bool
56
+ public let profiles: [StrategyProfileInfo]
57
+ }
58
+
59
+ private final class StrategyCommandCapture {
60
+ private let lock = NSLock()
61
+ private var data = Data()
62
+
63
+ func append(_ chunk: Data) {
64
+ guard !chunk.isEmpty else { return }
65
+ lock.lock()
66
+ data.append(chunk)
67
+ lock.unlock()
68
+ }
69
+
70
+ func string() -> String {
71
+ lock.lock()
72
+ let snapshot = data
73
+ lock.unlock()
74
+ return String(data: snapshot, encoding: .utf8) ?? ""
75
+ }
76
+ }
77
+
78
+ public enum StrategyModeService {
79
+ public static func load() throws -> StrategyModeSnapshot {
80
+ // `strategy show` deliberately returns 2 when the stored value is invalid.
81
+ // Accept that status so the app can still offer a supported profile to repair it.
82
+ let show = try run(["strategy", "show", "--json"], acceptedExitCodes: [0, 2])
83
+ guard let data = show.stdout.data(using: .utf8),
84
+ let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],
85
+ let configured = json["strategy"] as? String else {
86
+ throw serviceError(L("Unable to parse the current strategy.", "无法解析当前策略。"))
87
+ }
88
+
89
+ let enabled = (json["enabled"] as? NSNumber)?.boolValue ?? true
90
+ let routing = json["routing"] as? String
91
+ let valid = (json["valid"] as? NSNumber)?.boolValue ?? false
92
+ let profilesOutput = try run(["strategy", "profiles"])
93
+ let profiles = parseProfiles(profilesOutput.stdout)
94
+ guard !profiles.isEmpty else {
95
+ throw serviceError(L("No strategy profiles were reported by codex-flow.", "codex-flow 未返回可用策略模式。"))
96
+ }
97
+
98
+ return StrategyModeSnapshot(
99
+ enabled: enabled,
100
+ configured: configured,
101
+ routing: routing,
102
+ valid: valid,
103
+ profiles: profiles
104
+ )
105
+ }
106
+
107
+ public static func set(_ profile: String) throws -> StrategyModeSnapshot {
108
+ let before = try load()
109
+ guard before.profiles.contains(where: { $0.name == profile }) else {
110
+ throw serviceError(L("Unsupported strategy: \(profile)", "不支持的策略:\(profile)"))
111
+ }
112
+
113
+ _ = try run(["strategy", "set", profile])
114
+ let verified = try load()
115
+ guard verified.configured == profile, verified.valid else {
116
+ throw serviceError(L("Strategy change could not be verified.", "策略切换后无法验证配置结果。"))
117
+ }
118
+ return verified
119
+ }
120
+
121
+ public static func setEnabled(_ enabled: Bool) throws -> StrategyModeSnapshot {
122
+ _ = try run(["strategy", enabled ? "enable" : "disable"])
123
+ let verified = try load()
124
+ guard verified.enabled == enabled else {
125
+ throw serviceError(L(
126
+ "Strategy master switch change could not be verified.",
127
+ "策略总开关修改后无法验证配置结果。"
128
+ ))
129
+ }
130
+ return verified
131
+ }
132
+
133
+ public static func armTemporaryBypass() throws {
134
+ _ = try run(["strategy", "bypass-once"])
135
+ let pending = try run(["strategy", "bypass-pending"]).stdout
136
+ .trimmingCharacters(in: .whitespacesAndNewlines)
137
+ guard pending == "true" else {
138
+ throw serviceError(L(
139
+ "Temporary strategy bypass could not be verified.",
140
+ "临时关闭策略分发后无法验证一次性状态。"
141
+ ))
142
+ }
143
+ }
144
+
145
+ private struct CommandResult {
146
+ let stdout: String
147
+ let stderr: String
148
+ }
149
+
150
+ private static func run(
151
+ _ arguments: [String],
152
+ acceptedExitCodes: Set<Int32> = [0]
153
+ ) throws -> CommandResult {
154
+ let process = Process()
155
+ let output = Pipe()
156
+ let error = Pipe()
157
+ let environment = ProcessInfo.processInfo.environment
158
+ let home = FileManager.default.homeDirectoryForCurrentUser
159
+ let codexHome = environment["CODEX_HOME"]
160
+ .map(URL.init(fileURLWithPath:))
161
+ ?? home.appendingPathComponent(".codex")
162
+
163
+ // FlowPilot is commonly launched by Finder or a login item, where the
164
+ // process PATH does not include ~/.local/bin. Resolve the CLI locations
165
+ // used by install.sh directly before falling back to PATH lookup.
166
+ var candidates: [URL] = []
167
+ if let configuredBinDir = environment["CODEX_FLOW_BIN_DIR"], !configuredBinDir.isEmpty {
168
+ candidates.append(URL(fileURLWithPath: configuredBinDir).appendingPathComponent("codex-flow"))
169
+ }
170
+ candidates.append(home.appendingPathComponent(".local/bin/codex-flow"))
171
+ candidates.append(codexHome.appendingPathComponent("codex-flow/bin/codex-flow"))
172
+ candidates.append(URL(fileURLWithPath: "/opt/homebrew/bin/codex-flow"))
173
+ candidates.append(URL(fileURLWithPath: "/usr/local/bin/codex-flow"))
174
+
175
+ if let installedCLI = candidates.first(where: { FileManager.default.isExecutableFile(atPath: $0.path) }) {
176
+ process.executableURL = installedCLI
177
+ process.arguments = arguments
178
+ } else {
179
+ process.executableURL = URL(fileURLWithPath: "/usr/bin/env")
180
+ process.arguments = ["codex-flow"] + arguments
181
+ }
182
+
183
+ let stdoutCapture = StrategyCommandCapture()
184
+ let stderrCapture = StrategyCommandCapture()
185
+ let stdoutEOF = DispatchSemaphore(value: 0)
186
+ let stderrEOF = DispatchSemaphore(value: 0)
187
+
188
+ process.standardOutput = output
189
+ process.standardError = error
190
+ output.fileHandleForReading.readabilityHandler = { handle in
191
+ let data = handle.availableData
192
+ if data.isEmpty {
193
+ handle.readabilityHandler = nil
194
+ stdoutEOF.signal()
195
+ } else {
196
+ stdoutCapture.append(data)
197
+ }
198
+ }
199
+ error.fileHandleForReading.readabilityHandler = { handle in
200
+ let data = handle.availableData
201
+ if data.isEmpty {
202
+ handle.readabilityHandler = nil
203
+ stderrEOF.signal()
204
+ } else {
205
+ stderrCapture.append(data)
206
+ }
207
+ }
208
+ defer {
209
+ output.fileHandleForReading.readabilityHandler = nil
210
+ error.fileHandleForReading.readabilityHandler = nil
211
+ }
212
+
213
+ try process.run()
214
+ process.waitUntilExit()
215
+
216
+ // stdout/stderr are drained concurrently while the process runs, so a
217
+ // verbose child cannot fill a pipe and deadlock waitUntilExit(). Give
218
+ // the EOF callbacks a bounded moment to flush their final chunks.
219
+ _ = stdoutEOF.wait(timeout: .now() + 1.0)
220
+ _ = stderrEOF.wait(timeout: .now() + 1.0)
221
+
222
+ let stdout = stdoutCapture.string()
223
+ let stderr = stderrCapture.string()
224
+ guard acceptedExitCodes.contains(process.terminationStatus) else {
225
+ let detail = stderr.trimmingCharacters(in: .whitespacesAndNewlines)
226
+ throw serviceError(detail.isEmpty
227
+ ? L("codex-flow strategy command failed.", "codex-flow 策略命令执行失败。")
228
+ : detail)
229
+ }
230
+ return CommandResult(stdout: stdout, stderr: stderr)
231
+ }
232
+
233
+ private static func parseProfiles(_ raw: String) -> [StrategyProfileInfo] {
234
+ raw.split(whereSeparator: \.isNewline).compactMap { line in
235
+ let text = line.trimmingCharacters(in: .whitespacesAndNewlines)
236
+ guard !text.isEmpty else { return nil }
237
+ let pieces = text.split(maxSplits: 1, whereSeparator: \.isWhitespace)
238
+ guard let first = pieces.first else { return nil }
239
+ let name = String(first)
240
+ let description = pieces.count > 1 ? String(pieces[1]).trimmingCharacters(in: .whitespaces) : ""
241
+ return StrategyProfileInfo(name: name, description: description)
242
+ }
243
+ }
244
+
245
+ private static func serviceError(_ message: String) -> NSError {
246
+ NSError(domain: "FlowPilot.Strategy", code: 1, userInfo: [NSLocalizedDescriptionKey: message])
247
+ }
248
+ }
249
+
250
+ public struct StrategyModeCard: View {
251
+ @ObservedObject private var localization = AppLocalization.shared
252
+ @State private var snapshot: StrategyModeSnapshot?
253
+ @State private var isLoading = false
254
+ @State private var applyingProfile: String?
255
+ @State private var applyingEnabled = false
256
+ @State private var showDisableConfirmation = false
257
+ @State private var message: String?
258
+ @State private var isError = false
259
+
260
+ public init() {}
261
+
262
+ public var body: some View {
263
+ VStack(alignment: .leading, spacing: 7) {
264
+ HStack(spacing: 5) {
265
+ Label(L("Global strategy mode", "全局策略模式"), systemImage: "slider.horizontal.3")
266
+ .font(.system(size: 9.5, weight: .bold, design: .rounded))
267
+ .foregroundColor(.white.opacity(0.82))
268
+ Spacer()
269
+ if let snapshot {
270
+ if let routing = snapshot.routing, !routing.isEmpty {
271
+ Text(localizedRoutingName(routing))
272
+ .font(.system(size: 6.8, weight: .bold, design: .rounded))
273
+ .foregroundColor(.white.opacity(0.42))
274
+ }
275
+ Text(localizedConfiguredName(snapshot))
276
+ .font(.system(size: 7.5, weight: .heavy, design: .rounded))
277
+ .foregroundColor(statusColor(snapshot))
278
+ .padding(.horizontal, 6)
279
+ .padding(.vertical, 2)
280
+ .background(Capsule().fill(statusColor(snapshot).opacity(0.12)))
281
+ }
282
+ Button(action: refresh) {
283
+ Image(systemName: "arrow.clockwise")
284
+ .font(.system(size: 8, weight: .semibold))
285
+ .foregroundColor(.white.opacity(0.48))
286
+ }
287
+ .buttonStyle(.plain)
288
+ .disabled(isLoading || applyingProfile != nil || applyingEnabled)
289
+ }
290
+
291
+ if isLoading && snapshot == nil {
292
+ HStack(spacing: 6) {
293
+ ProgressView().controlSize(.mini)
294
+ Text(L("Reading strategy…", "正在读取策略…"))
295
+ .font(.system(size: 8.5))
296
+ .foregroundColor(.white.opacity(0.45))
297
+ }
298
+ .frame(maxWidth: .infinity, minHeight: 48)
299
+ } else if let snapshot {
300
+ HStack(spacing: 8) {
301
+ VStack(alignment: .leading, spacing: 2) {
302
+ Text(L("Enable strategy dispatch", "启用策略分发"))
303
+ .font(.system(size: 8.8, weight: .bold, design: .rounded))
304
+ .foregroundColor(.white.opacity(0.84))
305
+ Text(L(
306
+ "Turn off FlowPilot automatic planning and Worker dispatch while keeping the selected profile.",
307
+ "关闭 FlowPilot 自动策略规划和 Worker 分发,同时保留当前策略配置。"
308
+ ))
309
+ .font(.system(size: 7.2))
310
+ .foregroundColor(.white.opacity(0.36))
311
+ }
312
+ Spacer()
313
+ if applyingEnabled {
314
+ ProgressView().controlSize(.mini)
315
+ }
316
+ Toggle("", isOn: strategyEnabledBinding)
317
+ .labelsHidden()
318
+ .toggleStyle(SleekSwitchToggleStyle(tint: .cyan))
319
+ .disabled(isLoading || applyingProfile != nil || applyingEnabled)
320
+ }
321
+ .padding(7)
322
+ .background(RoundedRectangle(cornerRadius: 7).fill(Color.white.opacity(0.025)))
323
+
324
+ if !snapshot.valid {
325
+ Text(L("The stored strategy is invalid. Choose a supported mode below to repair it.", "当前保存的策略无效,请在下方选择一个受支持模式进行修复。"))
326
+ .font(.system(size: 7.8, weight: .medium))
327
+ .foregroundColor(.orange.opacity(0.88))
328
+ }
329
+
330
+ LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 6) {
331
+ ForEach(snapshot.profiles) { profile in
332
+ strategyButton(profile, current: snapshot.configured, enabled: snapshot.enabled)
333
+ }
334
+ }
335
+ .opacity(snapshot.enabled ? 1 : 0.42)
336
+
337
+ HStack(alignment: .top, spacing: 4) {
338
+ Image(systemName: "info.circle")
339
+ .font(.system(size: 7.5))
340
+ .padding(.top, 1)
341
+ Text(L("The master switch has global priority. When enabled, repository `.codex-flow.toml` may still override profile/routing; when disabled, repository policy cannot re-enable automatic dispatch.", "总开关具有全局最高优先级。开启时,仓库 `.codex-flow.toml` 仍可覆盖 profile/routing;关闭时,仓库策略不能重新开启自动分发。"))
342
+ .font(.system(size: 7.5))
343
+ }
344
+ .foregroundColor(.white.opacity(0.35))
345
+
346
+ if let message {
347
+ Text(message)
348
+ .font(.system(size: 8, weight: .medium))
349
+ .foregroundColor(isError ? .orange : .green)
350
+ .frame(maxWidth: .infinity, alignment: .leading)
351
+ }
352
+ } else {
353
+ Text(message ?? L("Strategy data unavailable", "策略数据暂不可用"))
354
+ .font(.system(size: 8.5, weight: .medium))
355
+ .foregroundColor(.orange.opacity(0.85))
356
+ .frame(maxWidth: .infinity, alignment: .leading)
357
+ }
358
+ }
359
+ .padding(9)
360
+ .background(
361
+ RoundedRectangle(cornerRadius: 10)
362
+ .fill(Color.white.opacity(0.04))
363
+ .overlay(RoundedRectangle(cornerRadius: 10).stroke(Color.white.opacity(0.08), lineWidth: 0.8))
364
+ )
365
+ .onAppear(perform: refresh)
366
+ .confirmationDialog(
367
+ L("Keep strategy dispatch available?", "要关闭策略分发吗?"),
368
+ isPresented: $showDisableConfirmation,
369
+ titleVisibility: .visible
370
+ ) {
371
+ Button(L("Temporary — next task only", "临时关闭 · 仅下一次任务")) {
372
+ armTemporaryBypass()
373
+ }
374
+ Button(L("Permanently disable", "永久关闭"), role: .destructive) {
375
+ applyEnabled(false)
376
+ }
377
+ Button(L("Cancel", "取消"), role: .cancel) {}
378
+ } message: {
379
+ Text(L(
380
+ "If you only want one task to run without FlowPilot distribution, use Temporary. Strategy dispatch will automatically return for the task after that.",
381
+ "如果只是想让下一次任务不经过 FlowPilot 分发,建议选择临时关闭;该任务消费后会自动恢复策略分发。"
382
+ ))
383
+ }
384
+ }
385
+
386
+ private var strategyEnabledBinding: Binding<Bool> {
387
+ Binding(
388
+ get: { snapshot?.enabled ?? true },
389
+ set: { requested in
390
+ if requested {
391
+ applyEnabled(true)
392
+ } else if snapshot?.enabled == true {
393
+ showDisableConfirmation = true
394
+ }
395
+ }
396
+ )
397
+ }
398
+
399
+ private func statusColor(_ snapshot: StrategyModeSnapshot) -> Color {
400
+ guard snapshot.enabled else { return .white.opacity(0.45) }
401
+ return snapshot.valid ? .cyan : .orange
402
+ }
403
+
404
+ private func localizedConfiguredName(_ snapshot: StrategyModeSnapshot) -> String {
405
+ guard snapshot.enabled else { return L("Off", "已关闭") }
406
+ return snapshot.profiles.first(where: { $0.name == snapshot.configured })?.localizedName
407
+ ?? snapshot.configured.capitalized
408
+ }
409
+
410
+ private func localizedRoutingName(_ routing: String) -> String {
411
+ switch routing.lowercased() {
412
+ case "adaptive": return L("Adaptive", "自适应")
413
+ case "direct": return L("Direct", "直接")
414
+ case "delegate": return L("Delegate", "委派")
415
+ default: return routing.capitalized
416
+ }
417
+ }
418
+
419
+ private func strategyButton(_ profile: StrategyProfileInfo, current: String, enabled: Bool) -> some View {
420
+ let selected = profile.name == current
421
+ let pending = applyingProfile == profile.name
422
+ return Button {
423
+ apply(profile.name)
424
+ } label: {
425
+ VStack(alignment: .leading, spacing: 4) {
426
+ HStack(spacing: 4) {
427
+ Image(systemName: profile.iconName)
428
+ .font(.system(size: 8.5, weight: .semibold))
429
+ .foregroundColor(profile.accent)
430
+ Text(profile.localizedName)
431
+ .font(.system(size: 8.8, weight: .bold, design: .rounded))
432
+ .foregroundColor(.white.opacity(selected ? 0.95 : 0.72))
433
+ Spacer()
434
+ if pending {
435
+ ProgressView().controlSize(.mini)
436
+ } else if selected {
437
+ Image(systemName: "checkmark.circle.fill")
438
+ .font(.system(size: 8.5))
439
+ .foregroundColor(profile.accent)
440
+ }
441
+ }
442
+ HoverRevealText(
443
+ profile.localizedDescription,
444
+ font: .system(size: 7.3),
445
+ foregroundColor: .white.opacity(0.38),
446
+ lineLimit: 2,
447
+ popoverWidth: 320
448
+ )
449
+ .frame(maxWidth: .infinity, alignment: .leading)
450
+ }
451
+ .padding(7)
452
+ .frame(maxWidth: .infinity, minHeight: 52, alignment: .leading)
453
+ .background(
454
+ RoundedRectangle(cornerRadius: 7)
455
+ .fill(selected ? profile.accent.opacity(0.12) : Color.white.opacity(0.025))
456
+ .overlay(
457
+ RoundedRectangle(cornerRadius: 7)
458
+ .stroke(selected ? profile.accent.opacity(0.45) : Color.white.opacity(0.055), lineWidth: 0.7)
459
+ )
460
+ )
461
+ }
462
+ .buttonStyle(.plain)
463
+ .disabled(applyingProfile != nil || applyingEnabled || !enabled || selected)
464
+ }
465
+
466
+ private func refresh() {
467
+ guard !isLoading, applyingProfile == nil, !applyingEnabled else { return }
468
+ isLoading = true
469
+ DispatchQueue.global(qos: .userInitiated).async {
470
+ do {
471
+ let value = try StrategyModeService.load()
472
+ DispatchQueue.main.async {
473
+ snapshot = value
474
+ message = nil
475
+ isError = false
476
+ isLoading = false
477
+ }
478
+ } catch {
479
+ DispatchQueue.main.async {
480
+ message = error.localizedDescription
481
+ isError = true
482
+ isLoading = false
483
+ }
484
+ }
485
+ }
486
+ }
487
+
488
+ private func apply(_ profile: String) {
489
+ guard applyingProfile == nil, !applyingEnabled else { return }
490
+ applyingProfile = profile
491
+ message = nil
492
+ DispatchQueue.global(qos: .userInitiated).async {
493
+ do {
494
+ let verified = try StrategyModeService.set(profile)
495
+ DispatchQueue.main.async {
496
+ snapshot = verified
497
+ applyingProfile = nil
498
+ isError = false
499
+ let displayName = verified.profiles.first(where: { $0.name == profile })?.localizedName ?? profile
500
+ message = L("Global strategy switched to \(displayName).", "全局策略已切换为 \(displayName)。")
501
+ }
502
+ } catch {
503
+ DispatchQueue.main.async {
504
+ applyingProfile = nil
505
+ isError = true
506
+ message = error.localizedDescription
507
+ }
508
+ }
509
+ }
510
+ }
511
+
512
+ private func armTemporaryBypass() {
513
+ guard !applyingEnabled, applyingProfile == nil else { return }
514
+ applyingEnabled = true
515
+ message = nil
516
+ DispatchQueue.global(qos: .userInitiated).async {
517
+ do {
518
+ try StrategyModeService.armTemporaryBypass()
519
+ DispatchQueue.main.async {
520
+ applyingEnabled = false
521
+ isError = false
522
+ message = L(
523
+ "Strategy dispatch will be skipped for the next task only.",
524
+ "已临时关闭:仅下一次任务跳过策略分发,之后自动恢复。"
525
+ )
526
+ }
527
+ } catch {
528
+ DispatchQueue.main.async {
529
+ applyingEnabled = false
530
+ isError = true
531
+ message = error.localizedDescription
532
+ }
533
+ }
534
+ }
535
+ }
536
+
537
+ private func applyEnabled(_ enabled: Bool) {
538
+ guard !applyingEnabled, applyingProfile == nil else { return }
539
+ applyingEnabled = true
540
+ message = nil
541
+ DispatchQueue.global(qos: .userInitiated).async {
542
+ do {
543
+ let verified = try StrategyModeService.setEnabled(enabled)
544
+ DispatchQueue.main.async {
545
+ snapshot = verified
546
+ applyingEnabled = false
547
+ isError = false
548
+ message = enabled
549
+ ? L("Strategy dispatch enabled.", "策略分发已开启。")
550
+ : L("Strategy dispatch disabled. FlowPilot will use ordinary Codex execution.", "策略分发已关闭,FlowPilot 将使用普通 Codex 执行。")
551
+ }
552
+ } catch {
553
+ DispatchQueue.main.async {
554
+ applyingEnabled = false
555
+ isError = true
556
+ message = error.localizedDescription
557
+ }
558
+ }
559
+ }
560
+ }
561
+ }