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,293 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
import Foundation
|
|
3
|
+
import Darwin
|
|
4
|
+
|
|
5
|
+
public struct FlowPilotAutostartStatus: Equatable {
|
|
6
|
+
public let enabled: Bool
|
|
7
|
+
public let plistExists: Bool
|
|
8
|
+
public let launchdLoaded: Bool
|
|
9
|
+
public let executablePath: String
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
public enum FlowPilotAutostartService {
|
|
13
|
+
public static let label = "com.parsifalc.codex-flow.flowpilot"
|
|
14
|
+
|
|
15
|
+
private static var environment: [String: String] { ProcessInfo.processInfo.environment }
|
|
16
|
+
private static var home: URL { FileManager.default.homeDirectoryForCurrentUser }
|
|
17
|
+
private static var codexHome: URL {
|
|
18
|
+
environment["CODEX_HOME"].map(URL.init(fileURLWithPath:))
|
|
19
|
+
?? home.appendingPathComponent(".codex")
|
|
20
|
+
}
|
|
21
|
+
private static var stateDirectory: URL { codexHome.appendingPathComponent("codex-flow") }
|
|
22
|
+
private static var preferenceURL: URL { stateDirectory.appendingPathComponent("autostart-preference") }
|
|
23
|
+
private static var launchAgentsDirectory: URL { home.appendingPathComponent("Library/LaunchAgents") }
|
|
24
|
+
private static var plistURL: URL { launchAgentsDirectory.appendingPathComponent("\(label).plist") }
|
|
25
|
+
|
|
26
|
+
private static let userDefaultsKey = "FlowPilot.Autostart.Enabled"
|
|
27
|
+
|
|
28
|
+
public static func status() -> FlowPilotAutostartStatus {
|
|
29
|
+
// Status is intentionally read-only. Re-registering a missing plist
|
|
30
|
+
// here can bootstrap a RunAtLoad LaunchAgent while the app is merely
|
|
31
|
+
// constructing its UI, which races the singleton owner.
|
|
32
|
+
let pref = configuredPreference()
|
|
33
|
+
let exists = FileManager.default.fileExists(atPath: plistURL.path)
|
|
34
|
+
// A stored preference alone is not an active login registration. A
|
|
35
|
+
// missing plist is reported as disabled so toggling the control back
|
|
36
|
+
// on explicitly recreates and bootstraps the LaunchAgent.
|
|
37
|
+
let isEnabled = (pref ?? exists) && exists
|
|
38
|
+
return FlowPilotAutostartStatus(
|
|
39
|
+
enabled: isEnabled,
|
|
40
|
+
plistExists: exists,
|
|
41
|
+
launchdLoaded: launchdIsLoaded(),
|
|
42
|
+
executablePath: resolvedExecutable().path
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@discardableResult
|
|
47
|
+
public static func enable() throws -> FlowPilotAutostartStatus {
|
|
48
|
+
try writePreference(enabled: true)
|
|
49
|
+
try writeRegistration()
|
|
50
|
+
return status()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
@discardableResult
|
|
54
|
+
public static func disable() throws -> FlowPilotAutostartStatus {
|
|
55
|
+
try writePreference(enabled: false)
|
|
56
|
+
// Deliberately do not `bootout` the current job here. If FlowPilot was
|
|
57
|
+
// launched by launchd, booting it out would kill the app as the user
|
|
58
|
+
// toggles the setting. Removing the plist is sufficient to prevent the
|
|
59
|
+
// next login from launching it; uninstall performs a full bootout.
|
|
60
|
+
try removeRegistration()
|
|
61
|
+
return status()
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private static func configuredPreference() -> Bool? {
|
|
65
|
+
if let raw = try? String(contentsOf: preferenceURL, encoding: .utf8)
|
|
66
|
+
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
67
|
+
.lowercased() {
|
|
68
|
+
switch raw {
|
|
69
|
+
case "enabled", "true", "1", "yes": return true
|
|
70
|
+
case "disabled", "false", "0", "no": return false
|
|
71
|
+
default: break
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if UserDefaults.standard.object(forKey: userDefaultsKey) != nil {
|
|
75
|
+
return UserDefaults.standard.bool(forKey: userDefaultsKey)
|
|
76
|
+
}
|
|
77
|
+
return nil
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private static func writePreference(enabled: Bool) throws {
|
|
81
|
+
try FileManager.default.createDirectory(at: stateDirectory, withIntermediateDirectories: true)
|
|
82
|
+
try (enabled ? "enabled\n" : "disabled\n").write(to: preferenceURL, atomically: true, encoding: .utf8)
|
|
83
|
+
UserDefaults.standard.set(enabled, forKey: userDefaultsKey)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private static func writeRegistration() throws {
|
|
87
|
+
let executable = resolvedExecutable()
|
|
88
|
+
guard FileManager.default.isExecutableFile(atPath: executable.path) else {
|
|
89
|
+
throw serviceError(L(
|
|
90
|
+
"FlowPilot executable is not available for login launch.",
|
|
91
|
+
"用于登录启动的 FlowPilot 可执行文件不存在。"
|
|
92
|
+
))
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
let logs = stateDirectory.appendingPathComponent("logs")
|
|
96
|
+
try FileManager.default.createDirectory(at: launchAgentsDirectory, withIntermediateDirectories: true)
|
|
97
|
+
try FileManager.default.createDirectory(at: logs, withIntermediateDirectories: true)
|
|
98
|
+
|
|
99
|
+
let path = [
|
|
100
|
+
home.appendingPathComponent(".local/bin").path,
|
|
101
|
+
"/opt/homebrew/bin",
|
|
102
|
+
"/usr/local/bin",
|
|
103
|
+
"/usr/bin",
|
|
104
|
+
"/bin",
|
|
105
|
+
"/usr/sbin",
|
|
106
|
+
"/sbin"
|
|
107
|
+
].joined(separator: ":")
|
|
108
|
+
|
|
109
|
+
var launchEnvironment: [String: String] = ["PATH": path]
|
|
110
|
+
// A RunAtLoad launch is best-effort reconciliation. The app must not
|
|
111
|
+
// preempt a manual/restart owner when launchd notices the plist.
|
|
112
|
+
launchEnvironment[FlowPilotInstanceLock.launchAgentEnvironmentKey] = "1"
|
|
113
|
+
if let configuredCodexHome = environment["CODEX_HOME"], !configuredCodexHome.isEmpty {
|
|
114
|
+
launchEnvironment["CODEX_HOME"] = configuredCodexHome
|
|
115
|
+
}
|
|
116
|
+
if let configuredBinDir = environment["CODEX_FLOW_BIN_DIR"], !configuredBinDir.isEmpty {
|
|
117
|
+
launchEnvironment["CODEX_FLOW_BIN_DIR"] = configuredBinDir
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
let plist: [String: Any] = [
|
|
121
|
+
"Label": label,
|
|
122
|
+
"ProgramArguments": [executable.path, "start"],
|
|
123
|
+
"RunAtLoad": true,
|
|
124
|
+
"KeepAlive": false,
|
|
125
|
+
"ProcessType": "Interactive",
|
|
126
|
+
"LimitLoadToSessionType": "Aqua",
|
|
127
|
+
"EnvironmentVariables": launchEnvironment,
|
|
128
|
+
"StandardOutPath": logs.appendingPathComponent("flowpilot-launchd.out.log").path,
|
|
129
|
+
"StandardErrorPath": logs.appendingPathComponent("flowpilot-launchd.err.log").path
|
|
130
|
+
]
|
|
131
|
+
|
|
132
|
+
let data = try PropertyListSerialization.data(fromPropertyList: plist, format: .xml, options: 0)
|
|
133
|
+
try data.write(to: plistURL, options: .atomic)
|
|
134
|
+
_ = chmod(plistURL.path, mode_t(S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH))
|
|
135
|
+
|
|
136
|
+
// Ensure launchd is aware of the LaunchAgent in the current GUI session
|
|
137
|
+
if !launchdIsLoaded() {
|
|
138
|
+
_ = runLaunchctl(["bootstrap", "gui/\(getuid())", plistURL.path])
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private static func removeRegistration() throws {
|
|
143
|
+
guard FileManager.default.fileExists(atPath: plistURL.path) else { return }
|
|
144
|
+
try FileManager.default.removeItem(at: plistURL)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private static func resolvedExecutable() -> URL {
|
|
148
|
+
// Prefer the stable installer-managed location so a source checkout can
|
|
149
|
+
// move without breaking login launch. Dev builds fall back to themselves.
|
|
150
|
+
let installed = stateDirectory.appendingPathComponent("bin/FlowPilot")
|
|
151
|
+
if FileManager.default.isExecutableFile(atPath: installed.path) {
|
|
152
|
+
return installed
|
|
153
|
+
}
|
|
154
|
+
let legacy = stateDirectory.appendingPathComponent("bin/codex-flow-overlay")
|
|
155
|
+
if FileManager.default.isExecutableFile(atPath: legacy.path) {
|
|
156
|
+
return legacy
|
|
157
|
+
}
|
|
158
|
+
if let current = CommandLine.arguments.first, !current.isEmpty {
|
|
159
|
+
let currentURL = URL(fileURLWithPath: current).standardizedFileURL
|
|
160
|
+
if FileManager.default.isExecutableFile(atPath: currentURL.path) {
|
|
161
|
+
return currentURL
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return installed
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private static func launchdIsLoaded() -> Bool {
|
|
168
|
+
runLaunchctl(["print", "gui/\(getuid())/\(label)"]) == 0
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private static func runLaunchctl(_ arguments: [String]) -> Int32 {
|
|
172
|
+
let process = Process()
|
|
173
|
+
process.executableURL = URL(fileURLWithPath: "/bin/launchctl")
|
|
174
|
+
process.arguments = arguments
|
|
175
|
+
process.standardOutput = FileHandle.nullDevice
|
|
176
|
+
process.standardError = FileHandle.nullDevice
|
|
177
|
+
do {
|
|
178
|
+
try process.run()
|
|
179
|
+
process.waitUntilExit()
|
|
180
|
+
return process.terminationStatus
|
|
181
|
+
} catch {
|
|
182
|
+
return -1
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private static func serviceError(_ message: String) -> NSError {
|
|
187
|
+
NSError(domain: "FlowPilot.Autostart", code: 1, userInfo: [NSLocalizedDescriptionKey: message])
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
public struct AutostartCard: View {
|
|
192
|
+
@ObservedObject private var localization = AppLocalization.shared
|
|
193
|
+
@State private var status = FlowPilotAutostartService.status()
|
|
194
|
+
@State private var isChanging = false
|
|
195
|
+
@State private var message: String?
|
|
196
|
+
@State private var isError = false
|
|
197
|
+
|
|
198
|
+
public init() {}
|
|
199
|
+
|
|
200
|
+
public var body: some View {
|
|
201
|
+
VStack(alignment: .leading, spacing: 7) {
|
|
202
|
+
HStack(spacing: 7) {
|
|
203
|
+
Image(systemName: "power.circle.fill")
|
|
204
|
+
.font(.system(size: 11, weight: .semibold))
|
|
205
|
+
.foregroundColor(status.enabled ? .green : .orange.opacity(0.72))
|
|
206
|
+
|
|
207
|
+
VStack(alignment: .leading, spacing: 2) {
|
|
208
|
+
Text(L("Launch at Login", "登录时启动"))
|
|
209
|
+
.font(.system(size: 9.5, weight: .bold, design: .rounded))
|
|
210
|
+
.foregroundColor(.white.opacity(0.84))
|
|
211
|
+
Text(statusText)
|
|
212
|
+
.font(.system(size: 7.6))
|
|
213
|
+
.foregroundColor(.white.opacity(0.42))
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
Spacer()
|
|
217
|
+
|
|
218
|
+
if isChanging {
|
|
219
|
+
ProgressView().controlSize(.mini)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
Toggle("", isOn: Binding(
|
|
223
|
+
get: { status.enabled },
|
|
224
|
+
set: { setEnabled($0) }
|
|
225
|
+
))
|
|
226
|
+
.labelsHidden()
|
|
227
|
+
.toggleStyle(SleekSwitchToggleStyle(tint: .green))
|
|
228
|
+
.disabled(isChanging)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if let message {
|
|
232
|
+
Text(message)
|
|
233
|
+
.font(.system(size: 7.6, weight: .medium))
|
|
234
|
+
.foregroundColor(isError ? .orange : .green)
|
|
235
|
+
.frame(maxWidth: .infinity, alignment: .leading)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
HStack(spacing: 4) {
|
|
239
|
+
Image(systemName: "info.circle")
|
|
240
|
+
.font(.system(size: 7.2))
|
|
241
|
+
Text(L(
|
|
242
|
+
"Registered as a user LaunchAgent; enabling it affects the next login and does not restart the current widget.",
|
|
243
|
+
"使用用户级 LaunchAgent 注册;开启后从下次登录生效,不会重启当前悬浮窗。"
|
|
244
|
+
))
|
|
245
|
+
.font(.system(size: 7.2))
|
|
246
|
+
}
|
|
247
|
+
.foregroundColor(.white.opacity(0.3))
|
|
248
|
+
}
|
|
249
|
+
.padding(9)
|
|
250
|
+
.background(
|
|
251
|
+
RoundedRectangle(cornerRadius: 10)
|
|
252
|
+
.fill(Color.white.opacity(0.04))
|
|
253
|
+
.overlay(RoundedRectangle(cornerRadius: 10).stroke(Color.white.opacity(0.08), lineWidth: 0.8))
|
|
254
|
+
)
|
|
255
|
+
.onAppear { status = FlowPilotAutostartService.status() }
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
private var statusText: String {
|
|
259
|
+
guard status.enabled else { return L("Disabled", "已关闭") }
|
|
260
|
+
if status.launchdLoaded {
|
|
261
|
+
return L("Enabled · managed by launchd in this login session", "已开启 · 当前登录会话由 launchd 托管")
|
|
262
|
+
}
|
|
263
|
+
return L("Enabled · starts automatically on next login", "已开启 · 下次登录自动启动")
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
private func setEnabled(_ enabled: Bool) {
|
|
267
|
+
guard !isChanging else { return }
|
|
268
|
+
isChanging = true
|
|
269
|
+
message = nil
|
|
270
|
+
DispatchQueue.global(qos: .userInitiated).async {
|
|
271
|
+
do {
|
|
272
|
+
let next = try enabled
|
|
273
|
+
? FlowPilotAutostartService.enable()
|
|
274
|
+
: FlowPilotAutostartService.disable()
|
|
275
|
+
DispatchQueue.main.async {
|
|
276
|
+
status = next
|
|
277
|
+
isChanging = false
|
|
278
|
+
isError = false
|
|
279
|
+
message = enabled
|
|
280
|
+
? L("FlowPilot will launch automatically when you next log in.", "FlowPilot 将在下次登录时自动启动。")
|
|
281
|
+
: L("Login launch disabled; FlowPilot remains running now.", "已关闭登录启动;当前 FlowPilot 继续运行。")
|
|
282
|
+
}
|
|
283
|
+
} catch {
|
|
284
|
+
DispatchQueue.main.async {
|
|
285
|
+
status = FlowPilotAutostartService.status()
|
|
286
|
+
isChanging = false
|
|
287
|
+
isError = true
|
|
288
|
+
message = error.localizedDescription
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
import AppKit
|
|
3
|
+
|
|
4
|
+
public struct BubbleView: View {
|
|
5
|
+
@ObservedObject var state: OverlayState
|
|
6
|
+
@ObservedObject private var localization = AppLocalization.shared
|
|
7
|
+
@ObservedObject private var updateService = FlowPilotUpdateService.shared
|
|
8
|
+
@State private var isHovered: Bool = false
|
|
9
|
+
@State private var breathePhase: CGFloat = 0.0
|
|
10
|
+
@State private var shimmerAngle: Double = 0.0
|
|
11
|
+
|
|
12
|
+
public init(state: OverlayState) {
|
|
13
|
+
self.state = state
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
public var body: some View {
|
|
17
|
+
ZStack {
|
|
18
|
+
if state.isDocked {
|
|
19
|
+
dockTabView
|
|
20
|
+
.transition(.opacity.combined(with: .scale(scale: 0.95)))
|
|
21
|
+
} else {
|
|
22
|
+
fullBubbleView
|
|
23
|
+
.transition(.opacity.combined(with: .scale(scale: 0.95)))
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if updateService.hasUpdateBadge {
|
|
27
|
+
residentUpdateBadge
|
|
28
|
+
.offset(x: updateBadgeOffsetX, y: -22)
|
|
29
|
+
.transition(.scale(scale: 0.7).combined(with: .opacity))
|
|
30
|
+
.allowsHitTesting(false)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
.frame(width: 76, height: 76)
|
|
34
|
+
.animation(.spring(response: 0.16, dampingFraction: 0.82), value: state.isDocked)
|
|
35
|
+
.animation(.spring(response: 0.22, dampingFraction: 0.78), value: updateService.hasUpdateBadge)
|
|
36
|
+
.onAppear {
|
|
37
|
+
withAnimation(.easeInOut(duration: 1.2).repeatForever(autoreverses: true)) {
|
|
38
|
+
breathePhase = 1.0
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
private func handleHover(_ hovered: Bool) {
|
|
44
|
+
withAnimation(.easeInOut(duration: 0.25)) {
|
|
45
|
+
isHovered = hovered
|
|
46
|
+
}
|
|
47
|
+
if hovered {
|
|
48
|
+
withAnimation(.linear(duration: 2.0).repeatForever(autoreverses: false)) {
|
|
49
|
+
shimmerAngle = 360.0
|
|
50
|
+
}
|
|
51
|
+
} else {
|
|
52
|
+
shimmerAngle = 0.0
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private var updateBadgeOffsetX: CGFloat {
|
|
57
|
+
if !state.isDocked { return -20 }
|
|
58
|
+
return state.dockEdge == .right ? 27 : -27
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
private var residentUpdateBadge: some View {
|
|
62
|
+
ZStack {
|
|
63
|
+
Circle()
|
|
64
|
+
.fill(updateService.isRestartRequired ? Color.orange : Color.red)
|
|
65
|
+
.frame(width: 12, height: 12)
|
|
66
|
+
.overlay(Circle().stroke(Color.black.opacity(0.78), lineWidth: 1.2))
|
|
67
|
+
.shadow(
|
|
68
|
+
color: (updateService.isRestartRequired ? Color.orange : Color.red).opacity(0.7),
|
|
69
|
+
radius: 3
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
Image(systemName: updateService.isRestartRequired ? "arrow.clockwise" : "arrow.down")
|
|
73
|
+
.font(.system(size: 6, weight: .black))
|
|
74
|
+
.foregroundColor(.white)
|
|
75
|
+
}
|
|
76
|
+
.accessibilityLabel(updateService.isRestartRequired ? L("Restart required", "需要重启") : L("Update available", "发现新版本"))
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// MARK: - Dedicated Edge Dock Tab View
|
|
80
|
+
private var dockTabView: some View {
|
|
81
|
+
HStack(spacing: 0) {
|
|
82
|
+
if state.dockEdge == .right {
|
|
83
|
+
Spacer(minLength: 0)
|
|
84
|
+
dockPillContent(isRight: true)
|
|
85
|
+
} else {
|
|
86
|
+
dockPillContent(isRight: false)
|
|
87
|
+
Spacer(minLength: 0)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
.frame(width: 76, height: 76)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private func dockPillContent(isRight: Bool) -> some View {
|
|
94
|
+
VStack(spacing: 2.5) {
|
|
95
|
+
HStack(spacing: 3) {
|
|
96
|
+
Circle()
|
|
97
|
+
.fill(statusColor)
|
|
98
|
+
.frame(width: 6.5, height: 6.5)
|
|
99
|
+
.shadow(color: statusColor.opacity(0.9), radius: 2)
|
|
100
|
+
|
|
101
|
+
Image(systemName: "sparkles")
|
|
102
|
+
.font(.system(size: 9.5, weight: .bold))
|
|
103
|
+
.foregroundStyle(
|
|
104
|
+
LinearGradient(
|
|
105
|
+
colors: [.cyan, .purple],
|
|
106
|
+
startPoint: .topLeading,
|
|
107
|
+
endPoint: .bottomTrailing
|
|
108
|
+
)
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
Text(state.latestRun?.formattedCompactTokens ?? L("Ready", "就绪"))
|
|
113
|
+
.font(.system(size: 9.0, weight: .bold, design: .rounded))
|
|
114
|
+
.foregroundColor(.white)
|
|
115
|
+
.lineLimit(1)
|
|
116
|
+
.minimumScaleFactor(0.7)
|
|
117
|
+
|
|
118
|
+
HStack(spacing: 2) {
|
|
119
|
+
if isRight {
|
|
120
|
+
Image(systemName: "chevron.left")
|
|
121
|
+
.font(.system(size: 7, weight: .bold))
|
|
122
|
+
.foregroundColor(.cyan.opacity(0.9))
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
Text(state.isTaskRunning ? L("RUN", "运行") : L("OK", "正常"))
|
|
126
|
+
.font(.system(size: 7.5, weight: .black, design: .rounded))
|
|
127
|
+
.foregroundColor(statusColor)
|
|
128
|
+
|
|
129
|
+
if !isRight {
|
|
130
|
+
Image(systemName: "chevron.right")
|
|
131
|
+
.font(.system(size: 7, weight: .bold))
|
|
132
|
+
.foregroundColor(.cyan.opacity(0.9))
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
.padding(.vertical, 7)
|
|
137
|
+
.padding(.horizontal, 5)
|
|
138
|
+
.frame(width: 44, height: 56)
|
|
139
|
+
.background(
|
|
140
|
+
RoundedRectangle(cornerRadius: 13, style: .continuous)
|
|
141
|
+
.fill(
|
|
142
|
+
LinearGradient(
|
|
143
|
+
colors: [
|
|
144
|
+
Color(red: 0.14, green: 0.16, blue: 0.22).opacity(0.96),
|
|
145
|
+
Color(red: 0.08, green: 0.09, blue: 0.13).opacity(0.98)
|
|
146
|
+
],
|
|
147
|
+
startPoint: isRight ? .leading : .trailing,
|
|
148
|
+
endPoint: isRight ? .trailing : .leading
|
|
149
|
+
)
|
|
150
|
+
)
|
|
151
|
+
)
|
|
152
|
+
.overlay(
|
|
153
|
+
RoundedRectangle(cornerRadius: 13, style: .continuous)
|
|
154
|
+
.stroke(
|
|
155
|
+
LinearGradient(
|
|
156
|
+
colors: [
|
|
157
|
+
Color.cyan.opacity(0.7),
|
|
158
|
+
Color.purple.opacity(0.5),
|
|
159
|
+
Color.white.opacity(0.15)
|
|
160
|
+
],
|
|
161
|
+
startPoint: isRight ? .leading : .trailing,
|
|
162
|
+
endPoint: isRight ? .trailing : .leading
|
|
163
|
+
),
|
|
164
|
+
lineWidth: 1.2
|
|
165
|
+
)
|
|
166
|
+
)
|
|
167
|
+
.shadow(color: statusGlowColor.opacity(0.45), radius: 4)
|
|
168
|
+
.contentShape(RoundedRectangle(cornerRadius: 13, style: .continuous))
|
|
169
|
+
.onHover { handleHover($0) }
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// MARK: - Full Circular Bubble View
|
|
173
|
+
private var fullBubbleView: some View {
|
|
174
|
+
ZStack {
|
|
175
|
+
Circle()
|
|
176
|
+
.fill(
|
|
177
|
+
RadialGradient(
|
|
178
|
+
colors: [
|
|
179
|
+
Color(red: 0.14, green: 0.16, blue: 0.22).opacity(0.92),
|
|
180
|
+
Color(red: 0.06, green: 0.07, blue: 0.10).opacity(0.97)
|
|
181
|
+
],
|
|
182
|
+
center: .center,
|
|
183
|
+
startRadius: 4,
|
|
184
|
+
endRadius: 29
|
|
185
|
+
)
|
|
186
|
+
)
|
|
187
|
+
.overlay(
|
|
188
|
+
Circle()
|
|
189
|
+
.stroke(Color.white.opacity(isHovered ? 0.25 : 0.12), lineWidth: 0.8)
|
|
190
|
+
)
|
|
191
|
+
.frame(width: 58, height: 58)
|
|
192
|
+
|
|
193
|
+
Circle()
|
|
194
|
+
.strokeBorder(
|
|
195
|
+
borderGradient,
|
|
196
|
+
lineWidth: isHovered ? 2.0 : 1.6
|
|
197
|
+
)
|
|
198
|
+
.rotationEffect(.degrees(isHovered ? shimmerAngle : 0))
|
|
199
|
+
.shadow(
|
|
200
|
+
color: statusGlowColor.opacity(isHovered ? 0.85 : (state.isTaskRunning ? 0.7 : 0.3)),
|
|
201
|
+
radius: isHovered ? 4 : 2
|
|
202
|
+
)
|
|
203
|
+
.frame(width: 58, height: 58)
|
|
204
|
+
|
|
205
|
+
VStack(spacing: 2) {
|
|
206
|
+
ZStack {
|
|
207
|
+
if state.isTaskRunning || isHovered {
|
|
208
|
+
Circle()
|
|
209
|
+
.stroke(statusColor.opacity(isHovered ? 0.6 : 0.4), lineWidth: 1.2)
|
|
210
|
+
.frame(width: 28, height: 28)
|
|
211
|
+
.scaleEffect(1.0 + breathePhase * (isHovered ? 0.25 : 0.35))
|
|
212
|
+
.opacity(1.0 - breathePhase)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
Image(systemName: "sparkles")
|
|
216
|
+
.font(.system(size: 18, weight: .semibold))
|
|
217
|
+
.foregroundStyle(
|
|
218
|
+
LinearGradient(
|
|
219
|
+
colors: isHovered ? [.white, .cyan, .purple] : [.cyan, .indigo, .purple],
|
|
220
|
+
startPoint: .topLeading,
|
|
221
|
+
endPoint: .bottomTrailing
|
|
222
|
+
)
|
|
223
|
+
)
|
|
224
|
+
.shadow(color: .cyan.opacity(isHovered ? 0.7 : 0.3), radius: isHovered ? 3 : 1.5)
|
|
225
|
+
}
|
|
226
|
+
.frame(height: 22)
|
|
227
|
+
|
|
228
|
+
HStack(spacing: 2) {
|
|
229
|
+
Image(systemName: "bolt.fill")
|
|
230
|
+
.font(.system(size: 6.5, weight: .bold))
|
|
231
|
+
.foregroundColor(statusColor)
|
|
232
|
+
|
|
233
|
+
Text(state.latestRun?.formattedCompactTokens ?? L("Ready", "就绪"))
|
|
234
|
+
.font(.system(size: 8, weight: .bold, design: .rounded))
|
|
235
|
+
.foregroundColor(.white.opacity(0.95))
|
|
236
|
+
.lineLimit(1)
|
|
237
|
+
.minimumScaleFactor(0.75)
|
|
238
|
+
}
|
|
239
|
+
.padding(.horizontal, 5)
|
|
240
|
+
.padding(.vertical, 1.5)
|
|
241
|
+
.background(
|
|
242
|
+
Capsule()
|
|
243
|
+
.fill(Color.black.opacity(0.65))
|
|
244
|
+
.overlay(
|
|
245
|
+
Capsule()
|
|
246
|
+
.stroke(Color.white.opacity(isHovered ? 0.3 : 0.15), lineWidth: 0.5)
|
|
247
|
+
)
|
|
248
|
+
)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
Circle()
|
|
252
|
+
.fill(statusColor)
|
|
253
|
+
.frame(width: 8, height: 8)
|
|
254
|
+
.overlay(
|
|
255
|
+
Circle()
|
|
256
|
+
.stroke(Color.black.opacity(0.7), lineWidth: 1.2)
|
|
257
|
+
)
|
|
258
|
+
.shadow(color: statusColor.opacity(isHovered ? 0.9 : 0.7), radius: isHovered ? 3 : 1.5)
|
|
259
|
+
.offset(x: 20, y: -20)
|
|
260
|
+
}
|
|
261
|
+
.frame(width: 58, height: 58)
|
|
262
|
+
.contentShape(Circle())
|
|
263
|
+
.onHover { handleHover($0) }
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
private var borderGradient: AnyShapeStyle {
|
|
267
|
+
if state.isTaskRunning {
|
|
268
|
+
return AnyShapeStyle(
|
|
269
|
+
AngularGradient(
|
|
270
|
+
colors: [.cyan, .blue, .teal, .cyan],
|
|
271
|
+
center: .center
|
|
272
|
+
)
|
|
273
|
+
)
|
|
274
|
+
} else if let run = state.latestRun, run.isError {
|
|
275
|
+
return AnyShapeStyle(
|
|
276
|
+
LinearGradient(
|
|
277
|
+
colors: [.orange, .red],
|
|
278
|
+
startPoint: .topLeading,
|
|
279
|
+
endPoint: .bottomTrailing
|
|
280
|
+
)
|
|
281
|
+
)
|
|
282
|
+
} else {
|
|
283
|
+
return AnyShapeStyle(
|
|
284
|
+
AngularGradient(
|
|
285
|
+
colors: [
|
|
286
|
+
Color.cyan.opacity(0.8),
|
|
287
|
+
Color.purple.opacity(0.8),
|
|
288
|
+
Color.pink.opacity(0.8),
|
|
289
|
+
Color.cyan.opacity(0.8)
|
|
290
|
+
],
|
|
291
|
+
center: .center
|
|
292
|
+
)
|
|
293
|
+
)
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
private var statusColor: Color {
|
|
298
|
+
if state.isTaskRunning {
|
|
299
|
+
return .cyan
|
|
300
|
+
} else if let run = state.latestRun {
|
|
301
|
+
if run.isError {
|
|
302
|
+
return .orange
|
|
303
|
+
}
|
|
304
|
+
return Color(red: 0.2, green: 0.85, blue: 0.45)
|
|
305
|
+
}
|
|
306
|
+
return Color(red: 0.2, green: 0.85, blue: 0.45)
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
private var statusGlowColor: Color {
|
|
310
|
+
if state.isTaskRunning {
|
|
311
|
+
return .cyan
|
|
312
|
+
} else if let run = state.latestRun, run.isError {
|
|
313
|
+
return .red
|
|
314
|
+
}
|
|
315
|
+
return Color(red: 0.2, green: 0.85, blue: 0.45)
|
|
316
|
+
}
|
|
317
|
+
}
|