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,352 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
|
|
3
|
+
public struct FlowPilotUpdateView: View {
|
|
4
|
+
@ObservedObject private var service = FlowPilotUpdateService.shared
|
|
5
|
+
|
|
6
|
+
public init() {}
|
|
7
|
+
|
|
8
|
+
public var body: some View {
|
|
9
|
+
VStack(alignment: .leading, spacing: 10) {
|
|
10
|
+
headerSection
|
|
11
|
+
|
|
12
|
+
ScrollView(.vertical, showsIndicators: true) {
|
|
13
|
+
VStack(alignment: .leading, spacing: 10) {
|
|
14
|
+
versionCard
|
|
15
|
+
autoUpdateCard
|
|
16
|
+
releaseNotesSection
|
|
17
|
+
feedbackMessageSection
|
|
18
|
+
}
|
|
19
|
+
.padding(.horizontal, 1)
|
|
20
|
+
}
|
|
21
|
+
.frame(maxHeight: 230)
|
|
22
|
+
|
|
23
|
+
if service.isRestartRequired {
|
|
24
|
+
restartSection
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
Divider()
|
|
28
|
+
.opacity(0.35)
|
|
29
|
+
|
|
30
|
+
actionButtons
|
|
31
|
+
}
|
|
32
|
+
.padding(14)
|
|
33
|
+
.frame(width: 330)
|
|
34
|
+
.onAppear {
|
|
35
|
+
service.refreshFromDisk()
|
|
36
|
+
service.requestCachedCheck()
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
private var headerSection: some View {
|
|
41
|
+
HStack(spacing: 8) {
|
|
42
|
+
ZStack {
|
|
43
|
+
Circle()
|
|
44
|
+
.fill(Color.cyan.opacity(0.16))
|
|
45
|
+
.frame(width: 34, height: 34)
|
|
46
|
+
Image(systemName: service.isRestartRequired ? "arrow.clockwise.circle.fill" : "arrow.down.circle.fill")
|
|
47
|
+
.font(.system(size: 17, weight: .semibold))
|
|
48
|
+
.foregroundColor(service.isRestartRequired ? .orange : .cyan)
|
|
49
|
+
}
|
|
50
|
+
VStack(alignment: .leading, spacing: 2) {
|
|
51
|
+
Text(L("Software Update", "软件更新"))
|
|
52
|
+
.font(.system(size: 13, weight: .bold, design: .rounded))
|
|
53
|
+
Text(service.statusText)
|
|
54
|
+
.font(.system(size: 9.5, weight: .medium))
|
|
55
|
+
.foregroundColor(.secondary)
|
|
56
|
+
.lineLimit(2)
|
|
57
|
+
}
|
|
58
|
+
Spacer(minLength: 4)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private var versionCard: some View {
|
|
63
|
+
VStack(spacing: 6) {
|
|
64
|
+
versionRow(
|
|
65
|
+
title: L("Current", "当前版本"),
|
|
66
|
+
value: service.snapshot.currentVersion.map { "v\($0)" } ?? "—"
|
|
67
|
+
)
|
|
68
|
+
versionRow(
|
|
69
|
+
title: L("Latest", "最新版本"),
|
|
70
|
+
value: service.snapshot.latestVersion.map { "v\($0)" } ?? "—"
|
|
71
|
+
)
|
|
72
|
+
versionRow(
|
|
73
|
+
title: L("Channel", "更新通道"),
|
|
74
|
+
value: service.snapshot.channel?.capitalized ?? "Stable"
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
.padding(9)
|
|
78
|
+
.background(
|
|
79
|
+
RoundedRectangle(cornerRadius: 9)
|
|
80
|
+
.fill(Color.primary.opacity(0.045))
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private var autoUpdateCard: some View {
|
|
85
|
+
HStack(spacing: 8) {
|
|
86
|
+
VStack(alignment: .leading, spacing: 2) {
|
|
87
|
+
Text(L("Automatic Updates", "自动更新"))
|
|
88
|
+
.font(.system(size: 10, weight: .bold))
|
|
89
|
+
Text(L("Automatically download, install, and restart", "发现新版本时自动下载安装并重启"))
|
|
90
|
+
.font(.system(size: 8))
|
|
91
|
+
.foregroundColor(.secondary)
|
|
92
|
+
}
|
|
93
|
+
Spacer()
|
|
94
|
+
if service.isChangingAutoUpdate {
|
|
95
|
+
ProgressView().controlSize(.mini)
|
|
96
|
+
}
|
|
97
|
+
Toggle("", isOn: Binding(
|
|
98
|
+
get: { service.isAutoUpdateEnabled },
|
|
99
|
+
set: { service.setAutoUpdateEnabled($0) }
|
|
100
|
+
))
|
|
101
|
+
.labelsHidden()
|
|
102
|
+
.toggleStyle(SleekSwitchToggleStyle(tint: .cyan, width: 32, height: 18))
|
|
103
|
+
.disabled(
|
|
104
|
+
service.isChangingAutoUpdate
|
|
105
|
+
|| service.isInstalling
|
|
106
|
+
|| service.isRestartingFlowPilot
|
|
107
|
+
|| service.isAutoRestartScheduled
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
.padding(8)
|
|
111
|
+
.background(
|
|
112
|
+
RoundedRectangle(cornerRadius: 8)
|
|
113
|
+
.fill(Color.primary.opacity(0.035))
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
@ViewBuilder
|
|
118
|
+
private var releaseNotesSection: some View {
|
|
119
|
+
if let notes = service.snapshot.releaseNotes, !notes.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
|
120
|
+
VStack(alignment: .leading, spacing: 5) {
|
|
121
|
+
HStack {
|
|
122
|
+
Text(L("What's New", "更新内容"))
|
|
123
|
+
.font(.system(size: 10, weight: .bold))
|
|
124
|
+
Spacer()
|
|
125
|
+
if let urlString = service.snapshot.releaseURL, let url = URL(string: urlString) {
|
|
126
|
+
Link(destination: url) {
|
|
127
|
+
HStack(spacing: 2) {
|
|
128
|
+
Text(L("Changelog", "更新日志"))
|
|
129
|
+
Image(systemName: "arrow.up.right")
|
|
130
|
+
}
|
|
131
|
+
.font(.system(size: 8.5, weight: .medium))
|
|
132
|
+
.foregroundColor(.cyan)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
ScrollView(.vertical, showsIndicators: true) {
|
|
138
|
+
Text(notes)
|
|
139
|
+
.font(.system(size: 9.5, weight: .regular))
|
|
140
|
+
.foregroundColor(.secondary)
|
|
141
|
+
.frame(maxWidth: .infinity, alignment: .leading)
|
|
142
|
+
.textSelection(.enabled)
|
|
143
|
+
.padding(8)
|
|
144
|
+
}
|
|
145
|
+
.frame(height: 96)
|
|
146
|
+
.background(
|
|
147
|
+
RoundedRectangle(cornerRadius: 8)
|
|
148
|
+
.fill(Color.primary.opacity(0.035))
|
|
149
|
+
)
|
|
150
|
+
.overlay(
|
|
151
|
+
RoundedRectangle(cornerRadius: 8)
|
|
152
|
+
.stroke(Color.primary.opacity(0.06), lineWidth: 1)
|
|
153
|
+
)
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
@ViewBuilder
|
|
159
|
+
private var feedbackMessageSection: some View {
|
|
160
|
+
if let error = service.actionError, !error.isEmpty {
|
|
161
|
+
HStack(alignment: .top, spacing: 6) {
|
|
162
|
+
Image(systemName: "exclamationmark.circle.fill")
|
|
163
|
+
.font(.system(size: 9.5))
|
|
164
|
+
.foregroundColor(.orange)
|
|
165
|
+
.padding(.top, 1)
|
|
166
|
+
ScrollView(.vertical, showsIndicators: true) {
|
|
167
|
+
Text(error)
|
|
168
|
+
.font(.system(size: 9, weight: .medium))
|
|
169
|
+
.foregroundColor(.orange)
|
|
170
|
+
.frame(maxWidth: .infinity, alignment: .leading)
|
|
171
|
+
}
|
|
172
|
+
.frame(maxHeight: 52)
|
|
173
|
+
}
|
|
174
|
+
.padding(7)
|
|
175
|
+
.background(
|
|
176
|
+
RoundedRectangle(cornerRadius: 7)
|
|
177
|
+
.fill(Color.orange.opacity(0.08))
|
|
178
|
+
)
|
|
179
|
+
} else if let message = service.actionMessage, !message.isEmpty {
|
|
180
|
+
Text(message)
|
|
181
|
+
.font(.system(size: 9, weight: .medium))
|
|
182
|
+
.foregroundColor(.secondary)
|
|
183
|
+
.lineLimit(3)
|
|
184
|
+
.fixedSize(horizontal: false, vertical: true)
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
@ViewBuilder
|
|
189
|
+
private var restartSection: some View {
|
|
190
|
+
VStack(alignment: .leading, spacing: 7) {
|
|
191
|
+
HStack(alignment: .top, spacing: 6) {
|
|
192
|
+
Image(systemName: "exclamationmark.triangle.fill")
|
|
193
|
+
.font(.system(size: 9))
|
|
194
|
+
.foregroundColor(.orange)
|
|
195
|
+
Text(restartExplanation)
|
|
196
|
+
.font(.system(size: 9, weight: .medium))
|
|
197
|
+
.foregroundColor(.secondary)
|
|
198
|
+
.fixedSize(horizontal: false, vertical: true)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
HStack(spacing: 6) {
|
|
202
|
+
if service.isFlowPilotRestartRequired {
|
|
203
|
+
Button {
|
|
204
|
+
service.restartFlowPilot()
|
|
205
|
+
} label: {
|
|
206
|
+
HStack(spacing: 4) {
|
|
207
|
+
if service.isRestartingFlowPilot || service.isAutoRestartScheduled {
|
|
208
|
+
ProgressView().controlSize(.mini)
|
|
209
|
+
} else {
|
|
210
|
+
Image(systemName: "arrow.clockwise")
|
|
211
|
+
}
|
|
212
|
+
Text(service.isAutoRestartScheduled ? L("Restarting…", "正在重启…") : L("Restart FlowPilot", "重启 FlowPilot"))
|
|
213
|
+
}
|
|
214
|
+
.font(.system(size: 9.5, weight: .semibold))
|
|
215
|
+
}
|
|
216
|
+
.buttonStyle(.borderedProminent)
|
|
217
|
+
.disabled(
|
|
218
|
+
service.isRestartingFlowPilot
|
|
219
|
+
|| service.isAutoRestartScheduled
|
|
220
|
+
|| service.isAcknowledgingRestart
|
|
221
|
+
|| service.isInstalling
|
|
222
|
+
|| service.isChecking
|
|
223
|
+
)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if service.isCodexRestartRequired {
|
|
227
|
+
Button {
|
|
228
|
+
service.acknowledgeRestart()
|
|
229
|
+
} label: {
|
|
230
|
+
HStack(spacing: 4) {
|
|
231
|
+
if service.isAcknowledgingRestart {
|
|
232
|
+
ProgressView().controlSize(.mini)
|
|
233
|
+
} else {
|
|
234
|
+
Image(systemName: "checkmark.circle")
|
|
235
|
+
}
|
|
236
|
+
Text(L("I've restarted Codex", "我已重启 Codex"))
|
|
237
|
+
}
|
|
238
|
+
.font(.system(size: 9.5, weight: .semibold))
|
|
239
|
+
}
|
|
240
|
+
.buttonStyle(.bordered)
|
|
241
|
+
.disabled(
|
|
242
|
+
service.isAcknowledgingRestart
|
|
243
|
+
|| service.isRestartingFlowPilot
|
|
244
|
+
|| service.isAutoRestartScheduled
|
|
245
|
+
|| service.isInstalling
|
|
246
|
+
|| service.isChecking
|
|
247
|
+
)
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
.padding(8)
|
|
252
|
+
.background(
|
|
253
|
+
RoundedRectangle(cornerRadius: 8)
|
|
254
|
+
.fill(Color.orange.opacity(0.07))
|
|
255
|
+
)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
private var actionButtons: some View {
|
|
259
|
+
HStack(spacing: 8) {
|
|
260
|
+
Button {
|
|
261
|
+
service.checkNow()
|
|
262
|
+
} label: {
|
|
263
|
+
HStack(spacing: 4) {
|
|
264
|
+
if service.isChecking {
|
|
265
|
+
ProgressView().controlSize(.mini)
|
|
266
|
+
} else {
|
|
267
|
+
Image(systemName: "arrow.clockwise")
|
|
268
|
+
}
|
|
269
|
+
Text(L("Check Again", "重新检查"))
|
|
270
|
+
}
|
|
271
|
+
.font(.system(size: 10, weight: .semibold))
|
|
272
|
+
.frame(maxWidth: .infinity, minHeight: 28)
|
|
273
|
+
}
|
|
274
|
+
.buttonStyle(.bordered)
|
|
275
|
+
.disabled(
|
|
276
|
+
service.isChecking
|
|
277
|
+
|| service.isInstalling
|
|
278
|
+
|| service.isAcknowledgingRestart
|
|
279
|
+
|| service.isRestartingFlowPilot
|
|
280
|
+
|| service.isAutoRestartScheduled
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
Button {
|
|
284
|
+
service.installUpdate()
|
|
285
|
+
} label: {
|
|
286
|
+
HStack(spacing: 4) {
|
|
287
|
+
if service.isInstalling {
|
|
288
|
+
ProgressView().controlSize(.mini)
|
|
289
|
+
} else {
|
|
290
|
+
Image(systemName: "arrow.down.circle.fill")
|
|
291
|
+
}
|
|
292
|
+
Text(service.isInstalling ? L("Updating…", "更新中…") : L("Update Now", "立即更新"))
|
|
293
|
+
}
|
|
294
|
+
.font(.system(size: 10, weight: .bold))
|
|
295
|
+
.frame(maxWidth: .infinity, minHeight: 28)
|
|
296
|
+
}
|
|
297
|
+
.buttonStyle(.borderedProminent)
|
|
298
|
+
.disabled(
|
|
299
|
+
service.isInstalling
|
|
300
|
+
|| service.isChecking
|
|
301
|
+
|| service.isAcknowledgingRestart
|
|
302
|
+
|| service.isRestartingFlowPilot
|
|
303
|
+
|| service.isAutoRestartScheduled
|
|
304
|
+
|| service.snapshot.updateAvailable != true
|
|
305
|
+
|| service.snapshot.artifactAvailable == false
|
|
306
|
+
)
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
private var restartExplanation: String {
|
|
311
|
+
if service.isFlowPilotRestartRequired && service.isCodexRestartRequired {
|
|
312
|
+
if let reason = service.codexRestartReason, !reason.isEmpty {
|
|
313
|
+
return L(
|
|
314
|
+
"The new files are installed (\(reason)). Restart FlowPilot to load the new app binary, and fully restart Codex to activate updated configurations.",
|
|
315
|
+
"新文件已经安装(\(reason))。请重启 FlowPilot 载入新的 App 程序,并完整重启 Codex 以激活新配置。"
|
|
316
|
+
)
|
|
317
|
+
}
|
|
318
|
+
return L(
|
|
319
|
+
"The new files are installed. Restart FlowPilot to load the new app binary, and fully restart Codex to activate updated FlowPilot policy and hook snapshots.",
|
|
320
|
+
"新文件已经安装。请重启 FlowPilot 载入新的 App 程序,并完整重启 Codex 以激活新的 FlowPilot 策略和 Hook 快照。"
|
|
321
|
+
)
|
|
322
|
+
}
|
|
323
|
+
if service.isFlowPilotRestartRequired {
|
|
324
|
+
return L(
|
|
325
|
+
"The updated FlowPilot binary is installed. Restart FlowPilot to load it.",
|
|
326
|
+
"新的 FlowPilot 程序已经安装。请重启 FlowPilot 以载入新版本。"
|
|
327
|
+
)
|
|
328
|
+
}
|
|
329
|
+
if let reason = service.codexRestartReason, !reason.isEmpty {
|
|
330
|
+
return L(
|
|
331
|
+
"Components updated (\(reason)). Fully restart Codex to activate updated configurations.",
|
|
332
|
+
"组件已更新(\(reason))。请完整重启 Codex 以激活新配置。"
|
|
333
|
+
)
|
|
334
|
+
}
|
|
335
|
+
return L(
|
|
336
|
+
"Fully restart Codex to activate updated FlowPilot policy and hook snapshots.",
|
|
337
|
+
"请完整重启 Codex,以激活新的 FlowPilot 策略和 Hook 快照。"
|
|
338
|
+
)
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
private func versionRow(title: String, value: String) -> some View {
|
|
342
|
+
HStack {
|
|
343
|
+
Text(title)
|
|
344
|
+
.font(.system(size: 9.5, weight: .medium))
|
|
345
|
+
.foregroundColor(.secondary)
|
|
346
|
+
Spacer()
|
|
347
|
+
Text(value)
|
|
348
|
+
.font(.system(size: 10, weight: .bold, design: .rounded))
|
|
349
|
+
.lineLimit(1)
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import Cocoa
|
|
2
|
+
import SwiftUI
|
|
3
|
+
import Darwin
|
|
4
|
+
|
|
5
|
+
class AppDelegate: NSObject, NSApplicationDelegate {
|
|
6
|
+
let instanceLock: FlowPilotInstanceLock
|
|
7
|
+
var state: OverlayState!
|
|
8
|
+
var windowController: OverlayWindowController!
|
|
9
|
+
var watcher: TelemetryWatcher!
|
|
10
|
+
var ipcServer: IPCService.Server!
|
|
11
|
+
|
|
12
|
+
init(instanceLock: FlowPilotInstanceLock) {
|
|
13
|
+
self.instanceLock = instanceLock
|
|
14
|
+
super.init()
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
func applicationDidFinishLaunching(_ notification: Notification) {
|
|
18
|
+
NSApp.setActivationPolicy(.accessory)
|
|
19
|
+
|
|
20
|
+
state = OverlayState()
|
|
21
|
+
windowController = OverlayWindowController(state: state)
|
|
22
|
+
watcher = TelemetryWatcher(state: state)
|
|
23
|
+
ipcServer = IPCService.Server(state: state)
|
|
24
|
+
|
|
25
|
+
// The singleton lock is acquired before NSApplication starts. If the
|
|
26
|
+
// IPC endpoint cannot be established, terminate through the normal
|
|
27
|
+
// lifecycle so `applicationWillTerminate` releases the lock too.
|
|
28
|
+
guard ipcServer.isRunning else {
|
|
29
|
+
writeStandardError(L(
|
|
30
|
+
"FlowPilot could not start its IPC server.\n",
|
|
31
|
+
"FlowPilot 无法启动 IPC 服务。\n"
|
|
32
|
+
))
|
|
33
|
+
NSApp.terminate(nil)
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
func applicationWillTerminate(_ notification: Notification) {
|
|
39
|
+
watcher?.stopWatching()
|
|
40
|
+
ipcServer?.stop()
|
|
41
|
+
// Release only after IPC shutdown has been requested. Keeping the
|
|
42
|
+
// lock through socket teardown prevents a replacement from racing
|
|
43
|
+
// this process's cancellation handler.
|
|
44
|
+
instanceLock.release()
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
func printUsage() {
|
|
49
|
+
print(L(
|
|
50
|
+
"""
|
|
51
|
+
FlowPilot - Native macOS Assistant & Telemetry Widget
|
|
52
|
+
|
|
53
|
+
Usage:
|
|
54
|
+
FlowPilot [command] [options]
|
|
55
|
+
|
|
56
|
+
Commands:
|
|
57
|
+
start, --daemon Start/Restart the FlowPilot floating daemon (default)
|
|
58
|
+
restart Restart the running FlowPilot daemon with fresh build
|
|
59
|
+
stop, quit Stop running FlowPilot daemon
|
|
60
|
+
status [--json] Check if FlowPilot daemon is currently active
|
|
61
|
+
autostart [action] Login launch: status|enable|disable
|
|
62
|
+
toggle Toggle between circular bubble and expanded summary
|
|
63
|
+
expand Expand summary window
|
|
64
|
+
collapse Collapse to circular bubble
|
|
65
|
+
tab <inspector|hist|stat> Switch active tab in expanded view
|
|
66
|
+
show <#|id|session> Inspect specific task in FlowPilot
|
|
67
|
+
stats [days] Open analytics dashboard with N days (default: 30)
|
|
68
|
+
history, list Open history task timeline
|
|
69
|
+
update [path|json] Push telemetry run update and expand
|
|
70
|
+
help, -h Show this help message
|
|
71
|
+
""",
|
|
72
|
+
"""
|
|
73
|
+
FlowPilot - macOS 原生助手与遥测悬浮窗
|
|
74
|
+
|
|
75
|
+
用法:
|
|
76
|
+
FlowPilot [命令] [选项]
|
|
77
|
+
|
|
78
|
+
命令:
|
|
79
|
+
start, --daemon 启动/重启 FlowPilot 悬浮窗(默认)
|
|
80
|
+
restart 使用最新构建重启 FlowPilot
|
|
81
|
+
stop, quit 停止 FlowPilot
|
|
82
|
+
status [--json] 查看 FlowPilot 是否正在运行
|
|
83
|
+
autostart [操作] 登录启动:status|enable|disable
|
|
84
|
+
toggle 在悬浮球与展开摘要之间切换
|
|
85
|
+
expand 展开摘要窗口
|
|
86
|
+
collapse 收起为悬浮球
|
|
87
|
+
tab <inspector|hist|stat> 切换展开视图中的标签页
|
|
88
|
+
show <#|id|session> 查看指定任务
|
|
89
|
+
stats [days] 打开 N 天统计面板(默认:30)
|
|
90
|
+
history, list 打开历史任务列表
|
|
91
|
+
update [path|json] 推送遥测任务更新并展开
|
|
92
|
+
help, -h 显示帮助
|
|
93
|
+
"""
|
|
94
|
+
))
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
func printAutostartStatus(_ status: FlowPilotAutostartStatus, json: Bool) {
|
|
98
|
+
if json {
|
|
99
|
+
let object: [String: Any] = [
|
|
100
|
+
"enabled": status.enabled,
|
|
101
|
+
"registered": status.plistExists,
|
|
102
|
+
"launchdLoaded": status.launchdLoaded,
|
|
103
|
+
"executable": status.executablePath
|
|
104
|
+
]
|
|
105
|
+
if let data = try? JSONSerialization.data(withJSONObject: object, options: [.sortedKeys]),
|
|
106
|
+
let text = String(data: data, encoding: .utf8) {
|
|
107
|
+
print(text)
|
|
108
|
+
}
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if status.enabled {
|
|
113
|
+
let detail = status.launchdLoaded
|
|
114
|
+
? L("launchd loaded in this login session", "当前登录会话已由 launchd 加载")
|
|
115
|
+
: L("registered for the next login", "已注册,将在下次登录时启动")
|
|
116
|
+
print(L("● Login launch enabled · \(detail)", "● 登录启动已开启 · \(detail)"))
|
|
117
|
+
} else {
|
|
118
|
+
print(L("○ Login launch disabled", "○ 登录启动已关闭"))
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
func printStatusFailureJSON(_ message: String) {
|
|
123
|
+
let object: [String: Any] = ["running": false, "error": message]
|
|
124
|
+
if let data = try? JSONSerialization.data(withJSONObject: object, options: [.sortedKeys]),
|
|
125
|
+
let text = String(data: data, encoding: .utf8) {
|
|
126
|
+
print(text)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
func writeStandardError(_ message: String) {
|
|
131
|
+
guard let data = message.data(using: .utf8) else { return }
|
|
132
|
+
FileHandle.standardError.write(data)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
func acquireFlowPilotInstanceLock(isExplicitRestart: Bool) -> FlowPilotInstanceLock? {
|
|
136
|
+
let lock: FlowPilotInstanceLock
|
|
137
|
+
do {
|
|
138
|
+
lock = try FlowPilotInstanceLock()
|
|
139
|
+
} catch {
|
|
140
|
+
writeStandardError(L(
|
|
141
|
+
"FlowPilot could not open its instance lock: \(error.localizedDescription)\n",
|
|
142
|
+
"FlowPilot 无法打开实例锁:\(error.localizedDescription)\n"
|
|
143
|
+
))
|
|
144
|
+
return nil
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
let isLaunchAgentStart = FlowPilotInstanceLock.isLaunchAgentStart
|
|
148
|
+
let timeoutNanos = UInt64(FlowPilotInstanceLock.manualHandoffTimeout * 1_000_000_000)
|
|
149
|
+
let now = DispatchTime.now().uptimeNanoseconds
|
|
150
|
+
let deadline = UInt64.max - now < timeoutNanos ? UInt64.max : now + timeoutNanos
|
|
151
|
+
var ownsLock = false
|
|
152
|
+
|
|
153
|
+
// A starter may race either a current lock-aware build or a legacy build
|
|
154
|
+
// that owns only the IPC socket. Do not proceed until this process owns
|
|
155
|
+
// the durable lock and no previous IPC owner is still answering.
|
|
156
|
+
while true {
|
|
157
|
+
let statusCheck = IPCService.sendCommand("status")
|
|
158
|
+
if statusCheck.success {
|
|
159
|
+
if isLaunchAgentStart && !isExplicitRestart {
|
|
160
|
+
// RunAtLoad must never preempt a live manual/restart owner,
|
|
161
|
+
// including an older build that predates the instance lock.
|
|
162
|
+
return nil
|
|
163
|
+
}
|
|
164
|
+
// An explicit restart is an active handoff even when its caller
|
|
165
|
+
// inherited the launchd marker. Ask the current IPC owner to
|
|
166
|
+
// terminate, then keep polling until its durable lock is released.
|
|
167
|
+
_ = IPCService.sendCommand("quit")
|
|
168
|
+
} else if ownsLock {
|
|
169
|
+
return lock
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if !ownsLock {
|
|
173
|
+
do {
|
|
174
|
+
try lock.acquire(timeout: 0)
|
|
175
|
+
ownsLock = true
|
|
176
|
+
} catch let error as FlowPilotInstanceLock.LockError {
|
|
177
|
+
switch error {
|
|
178
|
+
case .timedOut:
|
|
179
|
+
if isLaunchAgentStart && !isExplicitRestart {
|
|
180
|
+
// A loaded LaunchAgent may race a manual start before
|
|
181
|
+
// that owner has created IPC. Treat contention as a
|
|
182
|
+
// successful no-op because KeepAlive is disabled.
|
|
183
|
+
return nil
|
|
184
|
+
}
|
|
185
|
+
default:
|
|
186
|
+
writeStandardError(L(
|
|
187
|
+
"FlowPilot could not acquire its instance lock: \(error.localizedDescription)\n",
|
|
188
|
+
"FlowPilot 无法获取实例锁:\(error.localizedDescription)\n"
|
|
189
|
+
))
|
|
190
|
+
return nil
|
|
191
|
+
}
|
|
192
|
+
} catch {
|
|
193
|
+
writeStandardError(L(
|
|
194
|
+
"FlowPilot could not acquire its instance lock: \(error.localizedDescription)\n",
|
|
195
|
+
"FlowPilot 无法获取实例锁:\(error.localizedDescription)\n"
|
|
196
|
+
))
|
|
197
|
+
return nil
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if DispatchTime.now().uptimeNanoseconds >= deadline {
|
|
202
|
+
writeStandardError(L(
|
|
203
|
+
"FlowPilot restart timed out waiting for the previous instance to exit.\n",
|
|
204
|
+
"FlowPilot 重启等待旧实例退出超时。\n"
|
|
205
|
+
))
|
|
206
|
+
return nil
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
usleep(10_000)
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
let args = Array(CommandLine.arguments.dropFirst())
|
|
214
|
+
|
|
215
|
+
if args.isEmpty || args[0] == "start" || args[0] == "--daemon" || args[0] == "restart" {
|
|
216
|
+
// Detach from controlling terminal session and ignore hangup signals when running as daemon
|
|
217
|
+
_ = setsid()
|
|
218
|
+
signal(SIGHUP, SIG_IGN)
|
|
219
|
+
|
|
220
|
+
let isExplicitRestart = args.first == "restart"
|
|
221
|
+
guard let instanceLock = acquireFlowPilotInstanceLock(isExplicitRestart: isExplicitRestart) else {
|
|
222
|
+
// A LaunchAgent-created start is an expected no-op when another
|
|
223
|
+
// process owns the singleton lock. Explicit restarts must report a
|
|
224
|
+
// failed handoff instead, even if they inherited the launchd marker.
|
|
225
|
+
exit(FlowPilotInstanceLock.isLaunchAgentStart && !isExplicitRestart ? 0 : 1)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
let app = NSApplication.shared
|
|
229
|
+
let delegate = AppDelegate(instanceLock: instanceLock)
|
|
230
|
+
app.delegate = delegate
|
|
231
|
+
app.run()
|
|
232
|
+
} else {
|
|
233
|
+
let cmd = args[0]
|
|
234
|
+
switch cmd {
|
|
235
|
+
case "status":
|
|
236
|
+
let wantsJSON = args.contains("--json")
|
|
237
|
+
let res = IPCService.sendCommand("status")
|
|
238
|
+
if res.success {
|
|
239
|
+
if wantsJSON {
|
|
240
|
+
print(res.response)
|
|
241
|
+
} else {
|
|
242
|
+
print(L("● FlowPilot is RUNNING: \(res.response)", "● FlowPilot 正在运行:\(res.response)"))
|
|
243
|
+
}
|
|
244
|
+
exit(0)
|
|
245
|
+
} else {
|
|
246
|
+
if wantsJSON {
|
|
247
|
+
printStatusFailureJSON(res.response)
|
|
248
|
+
} else {
|
|
249
|
+
print(L("○ FlowPilot is NOT running.", "○ FlowPilot 未运行。"))
|
|
250
|
+
}
|
|
251
|
+
exit(1)
|
|
252
|
+
}
|
|
253
|
+
case "autostart":
|
|
254
|
+
let action = args.count > 1 ? args[1].lowercased() : "status"
|
|
255
|
+
let wantsJSON = args.contains("--json")
|
|
256
|
+
do {
|
|
257
|
+
switch action {
|
|
258
|
+
case "status":
|
|
259
|
+
printAutostartStatus(FlowPilotAutostartService.status(), json: wantsJSON)
|
|
260
|
+
case "enable", "on":
|
|
261
|
+
let status = try FlowPilotAutostartService.enable()
|
|
262
|
+
printAutostartStatus(status, json: wantsJSON)
|
|
263
|
+
case "disable", "off":
|
|
264
|
+
let status = try FlowPilotAutostartService.disable()
|
|
265
|
+
printAutostartStatus(status, json: wantsJSON)
|
|
266
|
+
default:
|
|
267
|
+
print(L(
|
|
268
|
+
"Usage: FlowPilot autostart status|enable|disable [--json]",
|
|
269
|
+
"用法:FlowPilot autostart status|enable|disable [--json]"
|
|
270
|
+
))
|
|
271
|
+
exit(2)
|
|
272
|
+
}
|
|
273
|
+
exit(0)
|
|
274
|
+
} catch {
|
|
275
|
+
if wantsJSON {
|
|
276
|
+
let object: [String: Any] = ["error": error.localizedDescription]
|
|
277
|
+
if let data = try? JSONSerialization.data(withJSONObject: object),
|
|
278
|
+
let text = String(data: data, encoding: .utf8) {
|
|
279
|
+
print(text)
|
|
280
|
+
}
|
|
281
|
+
} else {
|
|
282
|
+
print(L("Autostart update failed: \(error.localizedDescription)", "登录启动设置失败:\(error.localizedDescription)"))
|
|
283
|
+
}
|
|
284
|
+
exit(1)
|
|
285
|
+
}
|
|
286
|
+
case "toggle":
|
|
287
|
+
let res = IPCService.sendCommand("toggle")
|
|
288
|
+
print(res.response)
|
|
289
|
+
exit(res.success ? 0 : 1)
|
|
290
|
+
case "expand":
|
|
291
|
+
let res = IPCService.sendCommand("expand")
|
|
292
|
+
print(res.response)
|
|
293
|
+
exit(res.success ? 0 : 1)
|
|
294
|
+
case "collapse":
|
|
295
|
+
let res = IPCService.sendCommand("collapse")
|
|
296
|
+
print(res.response)
|
|
297
|
+
exit(res.success ? 0 : 1)
|
|
298
|
+
case "tab":
|
|
299
|
+
let target = args.dropFirst().joined(separator: " ")
|
|
300
|
+
let res = IPCService.sendCommand("tab \(target)")
|
|
301
|
+
print(res.response)
|
|
302
|
+
exit(res.success ? 0 : 1)
|
|
303
|
+
case "show":
|
|
304
|
+
let target = args.dropFirst().joined(separator: " ")
|
|
305
|
+
let res = IPCService.sendCommand("show \(target)")
|
|
306
|
+
print(res.response)
|
|
307
|
+
exit(res.success ? 0 : 1)
|
|
308
|
+
case "stats", "summary":
|
|
309
|
+
let target = args.dropFirst().joined(separator: " ")
|
|
310
|
+
let res = IPCService.sendCommand("stats \(target)")
|
|
311
|
+
print(res.response)
|
|
312
|
+
exit(res.success ? 0 : 1)
|
|
313
|
+
case "history", "list":
|
|
314
|
+
let res = IPCService.sendCommand("history")
|
|
315
|
+
print(res.response)
|
|
316
|
+
exit(res.success ? 0 : 1)
|
|
317
|
+
case "stop", "quit":
|
|
318
|
+
let res = IPCService.sendCommand("quit")
|
|
319
|
+
print(res.response)
|
|
320
|
+
exit(res.success ? 0 : 1)
|
|
321
|
+
case "update":
|
|
322
|
+
let payload = args.dropFirst().joined(separator: " ")
|
|
323
|
+
if payload.isEmpty {
|
|
324
|
+
let res = IPCService.sendCommand("update")
|
|
325
|
+
print(res.response)
|
|
326
|
+
exit(res.success ? 0 : 1)
|
|
327
|
+
} else {
|
|
328
|
+
let res = IPCService.sendCommand("update \(payload)")
|
|
329
|
+
print(res.response)
|
|
330
|
+
exit(res.success ? 0 : 1)
|
|
331
|
+
}
|
|
332
|
+
case "help", "-h", "--help":
|
|
333
|
+
printUsage()
|
|
334
|
+
exit(0)
|
|
335
|
+
default:
|
|
336
|
+
print(L("Unknown command: \(cmd)", "未知命令:\(cmd)"))
|
|
337
|
+
printUsage()
|
|
338
|
+
exit(1)
|
|
339
|
+
}
|
|
340
|
+
}
|