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,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
+ }