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