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,1052 @@
1
+ import Cocoa
2
+ import SwiftUI
3
+ import Combine
4
+
5
+ public enum DockEdge: String, Codable {
6
+ case none
7
+ case left
8
+ case right
9
+ }
10
+
11
+ // MARK: - Shared Observable State
12
+ public class OverlayState: ObservableObject {
13
+ @Published public var isExpanded: Bool = false
14
+ @Published public var isPinned: Bool = false
15
+ @Published public var isTaskRunning: Bool = false
16
+ @Published public var isDocked: Bool = false
17
+ @Published public var dockEdge: DockEdge = .right
18
+ @Published public var latestRun: TaskRun? = nil
19
+ @Published public var isPrivacyMode: Bool = false
20
+
21
+ @Published public var activeTab: OverlayTab = .inspector
22
+ @Published public var inspectedRun: TaskRun? = nil
23
+ @Published public var historyRuns: [TaskRun] = []
24
+ @Published public var historyChats: [ChatSession] = []
25
+ @Published public var expandedChatIds: Set<String> = []
26
+ @Published public var statsData: TelemetryStats? = nil
27
+ @Published public var statsDays: Int = 30
28
+ @Published public var selectedProject: String? = nil
29
+ @Published public var isTodayOnly: Bool = false
30
+ @Published public var searchQuery: String = ""
31
+ @Published public var recentChats: [ChatSession] = []
32
+ @Published public var allProjectsList: [String] = []
33
+
34
+ public weak var windowController: OverlayWindowController?
35
+
36
+ private var historyLoadGeneration = 0
37
+ private var statsLoadGeneration = 0
38
+
39
+ public init() {
40
+ if let latest = TelemetryQueryEngine.shared.loadLatestRun() {
41
+ latestRun = latest
42
+ isTaskRunning = latest.isRunning
43
+ }
44
+ loadMenuData()
45
+ }
46
+
47
+ public func loadMenuData() {
48
+ DispatchQueue.global(qos: .userInitiated).async {
49
+ let chats = TelemetryQueryEngine.shared.fetchChatHistory(limit: 15)
50
+ let projects = TelemetryQueryEngine.shared.allProjects()
51
+ DispatchQueue.main.async {
52
+ self.recentChats = chats
53
+ self.allProjectsList = projects
54
+ }
55
+ }
56
+ }
57
+
58
+ public func expand() {
59
+ guard !isExpanded else { return }
60
+ loadMenuData()
61
+ DispatchQueue.main.async {
62
+ self.windowController?.prepareForPresentationChange()
63
+ self.isDocked = false
64
+ self.isExpanded = true
65
+ self.windowController?.updateWindowFrame(animated: true)
66
+ }
67
+ }
68
+
69
+ public func collapse() {
70
+ guard isExpanded else { return }
71
+ DispatchQueue.main.async {
72
+ self.windowController?.prepareForPresentationChange()
73
+ self.isExpanded = false
74
+ self.isPinned = false
75
+ self.isDocked = false
76
+ // Reliability first: AppKit frame interpolation while SwiftUI swaps
77
+ // a 384x490 panel for a 76x76 bubble has been the source of several
78
+ // display/tracking races. Collapse now commits one stable frame;
79
+ // the compact circle/pill still animates entirely inside that host.
80
+ self.windowController?.updateWindowFrame(animated: false)
81
+ }
82
+ }
83
+
84
+ public func toggle() {
85
+ isExpanded ? collapse() : expand()
86
+ }
87
+
88
+ public func selectTab(_ tab: OverlayTab) {
89
+ DispatchQueue.main.async {
90
+ self.activeTab = tab
91
+ if tab == .history {
92
+ self.loadHistory()
93
+ } else if tab == .analytics {
94
+ self.loadStats()
95
+ }
96
+ self.windowController?.updateWindowFrame(animated: true)
97
+ }
98
+ }
99
+
100
+ public func inspect(run: TaskRun) {
101
+ var enrichedRun = run
102
+ DispatchQueue.main.async {
103
+ self.inspectedRun = enrichedRun
104
+ self.activeTab = .inspector
105
+ self.windowController?.updateWindowFrame(animated: true)
106
+ }
107
+ if enrichedRun.trajectory == nil || enrichedRun.skillsUsed == nil || enrichedRun.toolsUsed == nil || enrichedRun.logs == nil {
108
+ DispatchQueue.global(qos: .userInitiated).async {
109
+ TelemetryQueryEngine.shared.enrichRunIfNeeded(&enrichedRun)
110
+ DispatchQueue.main.async {
111
+ if self.inspectedRun?.id == enrichedRun.id {
112
+ self.inspectedRun = enrichedRun
113
+ }
114
+ }
115
+ }
116
+ }
117
+ }
118
+
119
+ public func jumpToLive() {
120
+ DispatchQueue.main.async {
121
+ self.inspectedRun = nil
122
+ self.activeTab = .inspector
123
+ self.windowController?.updateWindowFrame(animated: true)
124
+ }
125
+ }
126
+
127
+ public func toggleChatExpansion(_ id: String) {
128
+ if expandedChatIds.contains(id) {
129
+ expandedChatIds.remove(id)
130
+ } else {
131
+ expandedChatIds.insert(id)
132
+ }
133
+ windowController?.updateWindowFrame(animated: true)
134
+ }
135
+
136
+ public func isChatExpanded(_ id: String) -> Bool {
137
+ expandedChatIds.contains(id)
138
+ }
139
+
140
+ public func expandAllChats() {
141
+ expandedChatIds = Set(historyChats.map { $0.sessionId })
142
+ windowController?.updateWindowFrame(animated: true)
143
+ }
144
+
145
+ public func collapseAllChats() {
146
+ expandedChatIds.removeAll()
147
+ windowController?.updateWindowFrame(animated: true)
148
+ }
149
+
150
+ public func loadHistory() {
151
+ guard Thread.isMainThread else {
152
+ DispatchQueue.main.async { self.loadHistory() }
153
+ return
154
+ }
155
+
156
+ historyLoadGeneration += 1
157
+ let generation = historyLoadGeneration
158
+ let project = selectedProject
159
+ let todayOnly = isTodayOnly
160
+ let search = searchQuery
161
+
162
+ DispatchQueue.global(qos: .userInitiated).async {
163
+ let chats = TelemetryQueryEngine.shared.fetchChatHistory(
164
+ limit: 60,
165
+ project: project,
166
+ todayOnly: todayOnly,
167
+ search: search
168
+ )
169
+ let runs = TelemetryQueryEngine.shared.fetchHistory(
170
+ limit: 60,
171
+ project: project,
172
+ todayOnly: todayOnly,
173
+ search: search
174
+ )
175
+ DispatchQueue.main.async {
176
+ guard generation == self.historyLoadGeneration else { return }
177
+ self.historyChats = chats
178
+ self.historyRuns = runs
179
+ if self.expandedChatIds.isEmpty, let first = chats.first {
180
+ self.expandedChatIds.insert(first.sessionId)
181
+ }
182
+ }
183
+ }
184
+ }
185
+
186
+ public func loadStats() {
187
+ guard Thread.isMainThread else {
188
+ DispatchQueue.main.async { self.loadStats() }
189
+ return
190
+ }
191
+
192
+ statsLoadGeneration += 1
193
+ let generation = statsLoadGeneration
194
+ let days = statsDays
195
+ let project = selectedProject
196
+
197
+ DispatchQueue.global(qos: .userInitiated).async {
198
+ let stats = TelemetryQueryEngine.shared.computeStats(days: days, project: project)
199
+ DispatchQueue.main.async {
200
+ guard generation == self.statsLoadGeneration else { return }
201
+ self.statsData = stats
202
+ }
203
+ }
204
+ }
205
+
206
+ public func update(run: TaskRun) {
207
+ DispatchQueue.main.async {
208
+ self.latestRun = run
209
+ self.isTaskRunning = run.isRunning
210
+ if self.activeTab == .history {
211
+ self.loadHistory()
212
+ } else if self.activeTab == .analytics {
213
+ self.loadStats()
214
+ }
215
+ if self.isExpanded {
216
+ self.windowController?.updateWindowFrame(animated: true)
217
+ }
218
+ }
219
+ }
220
+ }
221
+
222
+ // MARK: - Main Root Container View
223
+ public struct OverlayRootView: View {
224
+ @ObservedObject var state: OverlayState
225
+
226
+ public init(state: OverlayState) {
227
+ self.state = state
228
+ }
229
+
230
+ public var body: some View {
231
+ ZStack(alignment: .topTrailing) {
232
+ if state.isExpanded {
233
+ SummaryView(state: state)
234
+ .frame(width: 384)
235
+ .transition(
236
+ .asymmetric(
237
+ insertion: .opacity.combined(with: .scale(scale: 0.95, anchor: .topTrailing)),
238
+ removal: .opacity.combined(with: .scale(scale: 0.95, anchor: .topTrailing))
239
+ )
240
+ )
241
+ } else {
242
+ BubbleView(state: state)
243
+ .frame(width: 76, height: 76)
244
+ .transition(
245
+ .asymmetric(
246
+ insertion: .opacity.combined(with: .scale(scale: 0.95, anchor: .topTrailing)),
247
+ removal: .opacity.combined(with: .scale(scale: 0.95, anchor: .topTrailing))
248
+ )
249
+ )
250
+ }
251
+ }
252
+ .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topTrailing)
253
+ .background(Color.clear)
254
+ .animation(.spring(response: 0.16, dampingFraction: 0.82), value: state.isExpanded)
255
+ }
256
+ }
257
+
258
+ // MARK: - Custom Tracking Hosting View
259
+ class TrackingHostingView<Content: View>: NSHostingView<Content> {
260
+ weak var windowController: OverlayWindowController?
261
+ private var trackingArea: NSTrackingArea?
262
+ private var logicalPointerInside = false
263
+ private var initialMouseScreenLocation: NSPoint = .zero
264
+ private var initialWindowOrigin: NSPoint = .zero
265
+ private var isDragging = false
266
+ private var ownsPointerInteraction = false
267
+
268
+ required public init(rootView: Content) {
269
+ super.init(rootView: rootView)
270
+ wantsLayer = true
271
+ layer?.backgroundColor = NSColor.clear.cgColor
272
+ layer?.isOpaque = false
273
+ }
274
+
275
+ required init?(coder: NSCoder) {
276
+ super.init(coder: coder)
277
+ wantsLayer = true
278
+ layer?.backgroundColor = NSColor.clear.cgColor
279
+ layer?.isOpaque = false
280
+ }
281
+
282
+ override func updateTrackingAreas() {
283
+ super.updateTrackingAreas()
284
+ if let trackingArea {
285
+ removeTrackingArea(trackingArea)
286
+ }
287
+ let options: NSTrackingArea.Options = [
288
+ .mouseEnteredAndExited,
289
+ .mouseMoved,
290
+ .activeAlways,
291
+ .inVisibleRect
292
+ ]
293
+ let area = NSTrackingArea(rect: bounds, options: options, owner: self, userInfo: nil)
294
+ addTrackingArea(area)
295
+ trackingArea = area
296
+ }
297
+
298
+ override func hitTest(_ point: NSPoint) -> NSView? {
299
+ guard let windowController else { return super.hitTest(point) }
300
+ guard windowController.isPointerInteractive(at: point, in: bounds) else { return nil }
301
+ return super.hitTest(point)
302
+ }
303
+
304
+ private func localPoint(for event: NSEvent) -> NSPoint {
305
+ convert(event.locationInWindow, from: nil)
306
+ }
307
+
308
+ private func routePointerMotion(_ event: NSEvent) {
309
+ guard let windowController else { return }
310
+ let point = localPoint(for: event)
311
+ let isInside = windowController.isPointerInteractive(at: point, in: bounds)
312
+
313
+ if isInside {
314
+ if logicalPointerInside {
315
+ windowController.handleMouseMoved(at: point)
316
+ } else {
317
+ logicalPointerInside = true
318
+ windowController.handleMouseEntered(at: point)
319
+ }
320
+ } else if logicalPointerInside {
321
+ logicalPointerInside = false
322
+ windowController.handleMouseExited()
323
+ }
324
+ }
325
+
326
+ override func mouseEntered(with event: NSEvent) {
327
+ super.mouseEntered(with: event)
328
+ routePointerMotion(event)
329
+ }
330
+
331
+ override func mouseMoved(with event: NSEvent) {
332
+ super.mouseMoved(with: event)
333
+ routePointerMotion(event)
334
+ }
335
+
336
+ override func mouseExited(with event: NSEvent) {
337
+ super.mouseExited(with: event)
338
+ if logicalPointerInside {
339
+ logicalPointerInside = false
340
+ windowController?.handleMouseExited()
341
+ }
342
+ }
343
+
344
+ // MARK: - Dragging & Magnetic Edge Snapping
345
+ override func mouseDown(with event: NSEvent) {
346
+ guard let window,
347
+ let windowController,
348
+ windowController.isPointerInteractive(at: localPoint(for: event), in: bounds),
349
+ windowController.beginPointerInteraction() else { return }
350
+
351
+ ownsPointerInteraction = true
352
+ initialMouseScreenLocation = NSEvent.mouseLocation
353
+ initialWindowOrigin = window.frame.origin
354
+ isDragging = false
355
+ windowController.cancelDwellTimer()
356
+ windowController.cancelTuckTimer()
357
+ super.mouseDown(with: event)
358
+ }
359
+
360
+ override func mouseDragged(with event: NSEvent) {
361
+ guard ownsPointerInteraction,
362
+ let window,
363
+ let windowController else {
364
+ super.mouseDragged(with: event)
365
+ return
366
+ }
367
+
368
+ let currentMouseScreenLocation = NSEvent.mouseLocation
369
+ let deltaX = currentMouseScreenLocation.x - initialMouseScreenLocation.x
370
+ let deltaY = currentMouseScreenLocation.y - initialMouseScreenLocation.y
371
+ let dragThreshold: CGFloat = windowController.state.isExpanded ? 8.0 : 4.0
372
+
373
+ if isDragging || abs(deltaX) > dragThreshold || abs(deltaY) > dragThreshold {
374
+ isDragging = true
375
+ windowController.cancelDwellTimer()
376
+ windowController.cancelTuckTimer()
377
+
378
+ var newOrigin = NSPoint(
379
+ x: initialWindowOrigin.x + deltaX,
380
+ y: initialWindowOrigin.y + deltaY
381
+ )
382
+
383
+ if let visible = windowController.visibleFrameForDrag(
384
+ mouseLocation: currentMouseScreenLocation,
385
+ windowFrame: window.frame
386
+ ) {
387
+ newOrigin = OverlayScreenGeometry.clamp(
388
+ newOrigin,
389
+ windowSize: window.frame.size,
390
+ to: visible
391
+ )
392
+ }
393
+ window.setFrameOrigin(newOrigin)
394
+ } else {
395
+ super.mouseDragged(with: event)
396
+ }
397
+ }
398
+
399
+ override func mouseUp(with event: NSEvent) {
400
+ guard ownsPointerInteraction else {
401
+ super.mouseUp(with: event)
402
+ return
403
+ }
404
+
405
+ ownsPointerInteraction = false
406
+ let dragged = isDragging
407
+ isDragging = false
408
+
409
+ if dragged, let window, let windowController {
410
+ windowController.endPointerInteraction(drainPendingPresentation: false)
411
+ windowController.performMagneticSnap(
412
+ for: window,
413
+ pointerLocation: NSEvent.mouseLocation
414
+ )
415
+ } else if let windowController {
416
+ windowController.endPointerInteraction(drainPendingPresentation: true)
417
+ if !windowController.state.isExpanded {
418
+ windowController.state.expand()
419
+ }
420
+ }
421
+ super.mouseUp(with: event)
422
+ }
423
+
424
+ override func rightMouseDown(with event: NSEvent) {
425
+ guard let windowController,
426
+ windowController.isPointerInteractive(at: localPoint(for: event), in: bounds) else { return }
427
+
428
+ let menu = NSMenu()
429
+ let state = windowController.state
430
+ if state.isExpanded {
431
+ let pinItem = NSMenuItem(
432
+ title: state.isPinned ? L("Unpin Window", "取消置顶") : L("Pin Window", "置顶窗口"),
433
+ action: #selector(togglePin),
434
+ keyEquivalent: "p"
435
+ )
436
+ pinItem.target = self
437
+ menu.addItem(pinItem)
438
+
439
+ let collapseItem = NSMenuItem(
440
+ title: L("Collapse to Bubble", "收起为悬浮球"),
441
+ action: #selector(collapseBubble),
442
+ keyEquivalent: "c"
443
+ )
444
+ collapseItem.target = self
445
+ menu.addItem(collapseItem)
446
+ } else {
447
+ let expandItem = NSMenuItem(
448
+ title: L("Expand Summary", "展开摘要"),
449
+ action: #selector(expandSummary),
450
+ keyEquivalent: "e"
451
+ )
452
+ expandItem.target = self
453
+ menu.addItem(expandItem)
454
+ }
455
+
456
+ menu.addItem(NSMenuItem.separator())
457
+
458
+ let consoleItem = NSMenuItem(
459
+ title: L("Open FlowPilot Console", "打开 FlowPilot 控制台"),
460
+ action: #selector(openConsole),
461
+ keyEquivalent: "t"
462
+ )
463
+ consoleItem.target = self
464
+ menu.addItem(consoleItem)
465
+
466
+ let refreshItem = NSMenuItem(
467
+ title: L("Refresh Telemetry", "刷新遥测数据"),
468
+ action: #selector(refreshData),
469
+ keyEquivalent: "r"
470
+ )
471
+ refreshItem.target = self
472
+ menu.addItem(refreshItem)
473
+
474
+ menu.addItem(NSMenuItem.separator())
475
+
476
+ let quitItem = NSMenuItem(
477
+ title: L("Quit FlowPilot", "退出 FlowPilot"),
478
+ action: #selector(quitApp),
479
+ keyEquivalent: "q"
480
+ )
481
+ quitItem.target = self
482
+ menu.addItem(quitItem)
483
+
484
+ NSMenu.popUpContextMenu(menu, with: event, for: self)
485
+ }
486
+
487
+ @objc private func togglePin() {
488
+ windowController?.state.isPinned.toggle()
489
+ }
490
+
491
+ @objc private func expandSummary() {
492
+ windowController?.state.expand()
493
+ }
494
+
495
+ @objc private func collapseBubble() {
496
+ windowController?.state.collapse()
497
+ }
498
+
499
+ @objc private func openConsole() {
500
+ let script = """
501
+ tell application "Terminal"
502
+ activate
503
+ do script "codex-flow"
504
+ end tell
505
+ """
506
+ if let appleScript = NSAppleScript(source: script) {
507
+ var error: NSDictionary?
508
+ appleScript.executeAndReturnError(&error)
509
+ }
510
+ }
511
+
512
+ @objc private func refreshData() {
513
+ windowController?.refreshTelemetry()
514
+ }
515
+
516
+ @objc private func quitApp() {
517
+ NSApplication.shared.terminate(nil)
518
+ }
519
+ }
520
+
521
+ // MARK: - Overlay Window Controller
522
+ public class OverlayWindowController: NSObject, NSWindowDelegate {
523
+ public let state: OverlayState
524
+ public var window: NSPanel!
525
+
526
+ public var isInteractingOrDragging: Bool {
527
+ runtime.pointerInteractionActive
528
+ }
529
+
530
+ private var hoverDwellTimer: Timer?
531
+ private var collapseTimer: Timer?
532
+ private var tuckTimer: Timer?
533
+ private let edgeTuckIdleInterval: TimeInterval = 30.0
534
+
535
+ private var runtime = OverlayRuntimeState()
536
+ private var hoverGate = OverlayHoverGate(rearmDistance: 6)
537
+ private var pendingPresentationAnimated = true
538
+ private var needsPointerReconciliationAfterGeometry = false
539
+
540
+ private let bubbleSize = NSSize(width: 76, height: 76)
541
+ private let summarySize = NSSize(width: 384, height: 490)
542
+ private let snapMargin: CGFloat = 8.0
543
+ private let snapThreshold: CGFloat = 36.0
544
+
545
+ public init(state: OverlayState) {
546
+ self.state = state
547
+ super.init()
548
+ state.windowController = self
549
+ setupWindow()
550
+ }
551
+
552
+ private var visibleFrames: [NSRect] {
553
+ NSScreen.screens.map { $0.visibleFrame }
554
+ }
555
+
556
+ private var isGeometryTransitioning: Bool {
557
+ runtime.isGeometryTransitioning
558
+ }
559
+
560
+ private func resolvedVisibleFrame(for frame: NSRect, preferredPoint: NSPoint? = nil) -> NSRect? {
561
+ let frames = visibleFrames
562
+ if let preferredPoint,
563
+ let byPointer = OverlayScreenGeometry.visibleFrame(containing: preferredPoint, among: frames) {
564
+ return byPointer
565
+ }
566
+ if let byFrame = OverlayScreenGeometry.bestVisibleFrame(for: frame, among: frames) {
567
+ return byFrame
568
+ }
569
+ return NSScreen.main?.visibleFrame
570
+ }
571
+
572
+ private func presentationVisibleFrame(for frame: NSRect) -> NSRect? {
573
+ OverlayScreenGeometry.presentationVisibleFrame(for: frame, among: visibleFrames)
574
+ ?? NSScreen.main?.visibleFrame
575
+ }
576
+
577
+ func visibleFrameForDrag(mouseLocation: NSPoint, windowFrame: NSRect) -> NSRect? {
578
+ resolvedVisibleFrame(for: windowFrame, preferredPoint: mouseLocation)
579
+ }
580
+
581
+ private func setupWindow() {
582
+ state.dockEdge = .right
583
+ let restored = loadSavedPosition() ?? defaultPosition(for: bubbleSize)
584
+ let initialRect = normalizedCollapsedFrame(restored)
585
+
586
+ window = NSPanel(
587
+ contentRect: initialRect,
588
+ styleMask: [.borderless, .nonactivatingPanel],
589
+ backing: .buffered,
590
+ defer: false
591
+ )
592
+ window.level = .floating
593
+ window.isFloatingPanel = true
594
+ window.isOpaque = false
595
+ window.backgroundColor = .clear
596
+ window.hasShadow = false
597
+ window.hidesOnDeactivate = false
598
+ window.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary, .stationary]
599
+ window.isMovableByWindowBackground = false
600
+ window.delegate = self
601
+
602
+ let hostingView = TrackingHostingView(rootView: OverlayRootView(state: state))
603
+ hostingView.windowController = self
604
+ window.contentView = hostingView
605
+ window.orderFrontRegardless()
606
+
607
+ DispatchQueue.main.asyncAfter(deadline: .now() + 1.2) { [weak self] in
608
+ self?.scheduleTuck()
609
+ }
610
+ }
611
+
612
+ public func prepareForPresentationChange() {
613
+ cancelDwellTimer()
614
+ cancelTuckTimer()
615
+ collapseTimer?.invalidate()
616
+ collapseTimer = nil
617
+ }
618
+
619
+ // MARK: - Pointer ownership / hit testing
620
+ func isPointerInteractive(at point: NSPoint, in hostBounds: NSRect) -> Bool {
621
+ OverlayCompactHitRegion.contains(
622
+ point,
623
+ in: hostBounds,
624
+ expanded: state.isExpanded,
625
+ docked: state.isDocked
626
+ )
627
+ }
628
+
629
+ @discardableResult
630
+ func beginPointerInteraction() -> Bool {
631
+ runtime.beginPointerInteraction()
632
+ }
633
+
634
+ func endPointerInteraction(drainPendingPresentation: Bool) {
635
+ runtime.endPointerInteraction()
636
+ guard drainPendingPresentation,
637
+ runtime.claimPendingPresentationIfIdle() else { return }
638
+ performPresentationFrameUpdate(animated: pendingPresentationAnimated)
639
+ }
640
+
641
+ // MARK: - Single-owner geometry pipeline
642
+ public func updateWindowFrame(animated: Bool = true) {
643
+ pendingPresentationAnimated = animated
644
+ guard runtime.requestPresentationGeometry() else { return }
645
+ performPresentationFrameUpdate(animated: animated)
646
+ }
647
+
648
+ private func performPresentationFrameUpdate(animated: Bool) {
649
+ guard let window else {
650
+ finishGeometryActivity()
651
+ return
652
+ }
653
+
654
+ let targetSize = state.isExpanded ? summarySize : bubbleSize
655
+ let currentFrame = window.frame
656
+ guard let visible = presentationVisibleFrame(for: currentFrame) else {
657
+ finishGeometryActivity()
658
+ return
659
+ }
660
+
661
+ var newOrigin = NSPoint(
662
+ x: currentFrame.maxX - targetSize.width,
663
+ y: currentFrame.maxY - targetSize.height
664
+ )
665
+
666
+ if state.isExpanded {
667
+ newOrigin = OverlayScreenGeometry.clamp(
668
+ newOrigin,
669
+ windowSize: targetSize,
670
+ to: visible
671
+ )
672
+ } else {
673
+ state.dockEdge = .right
674
+ newOrigin.x = visible.maxX - targetSize.width
675
+ newOrigin.y = max(visible.minY, min(newOrigin.y, visible.maxY - targetSize.height))
676
+ }
677
+
678
+ let targetFrame = NSRect(origin: newOrigin, size: targetSize)
679
+ let collapsedAfterAnimation = !state.isExpanded
680
+ if collapsedAfterAnimation {
681
+ suppressHoverUntilPointerMoves()
682
+ }
683
+
684
+ let completed: () -> Void = { [weak self] in
685
+ guard let self else { return }
686
+ self.window.orderFrontRegardless()
687
+ self.presentationFrameDidSet(targetOrigin: newOrigin, collapsed: collapsedAfterAnimation)
688
+ let startedNext = self.finishGeometryActivity()
689
+ if !startedNext, collapsedAfterAnimation, !self.state.isExpanded {
690
+ self.scheduleTuck()
691
+ }
692
+ }
693
+
694
+ if approximatelyEqual(window.frame, targetFrame) || !animated {
695
+ window.setFrame(targetFrame, display: true)
696
+ completed()
697
+ return
698
+ }
699
+
700
+ NSAnimationContext.runAnimationGroup({ context in
701
+ context.duration = 0.16
702
+ context.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
703
+ context.allowsImplicitAnimation = true
704
+ window.animator().setFrame(targetFrame, display: true)
705
+ }, completionHandler: completed)
706
+ }
707
+
708
+ private func presentationFrameDidSet(targetOrigin: NSPoint, collapsed: Bool) {
709
+ if collapsed && !state.isExpanded {
710
+ saveWindowPosition(targetOrigin)
711
+ }
712
+ }
713
+
714
+ @discardableResult
715
+ private func finishGeometryActivity() -> Bool {
716
+ let shouldRunPendingPresentation = runtime.completeGeometry()
717
+ if shouldRunPendingPresentation {
718
+ performPresentationFrameUpdate(animated: pendingPresentationAnimated)
719
+ return true
720
+ }
721
+ reconcilePointerAfterGeometryIfNeeded()
722
+ return false
723
+ }
724
+
725
+ private func reconcilePointerAfterGeometryIfNeeded() {
726
+ guard needsPointerReconciliationAfterGeometry,
727
+ let contentView = window?.contentView,
728
+ let window else { return }
729
+ needsPointerReconciliationAfterGeometry = false
730
+
731
+ let pointInWindow = window.convertPoint(fromScreen: NSEvent.mouseLocation)
732
+ let pointInHost = contentView.convert(pointInWindow, from: nil)
733
+ if isPointerInteractive(at: pointInHost, in: contentView.bounds) {
734
+ handleMouseEntered(at: pointInHost)
735
+ } else {
736
+ handleMouseExited()
737
+ }
738
+ }
739
+
740
+ private func approximatelyEqual(_ lhs: NSRect, _ rhs: NSRect, tolerance: CGFloat = 0.5) -> Bool {
741
+ abs(lhs.origin.x - rhs.origin.x) <= tolerance &&
742
+ abs(lhs.origin.y - rhs.origin.y) <= tolerance &&
743
+ abs(lhs.size.width - rhs.size.width) <= tolerance &&
744
+ abs(lhs.size.height - rhs.size.height) <= tolerance
745
+ }
746
+
747
+ // MARK: - Edge Half-Tuck Support
748
+ public func cancelTuckTimer() {
749
+ tuckTimer?.invalidate()
750
+ tuckTimer = nil
751
+ }
752
+
753
+ public func scheduleTuck() {
754
+ cancelTuckTimer()
755
+ guard !state.isExpanded,
756
+ !state.isPinned,
757
+ !isInteractingOrDragging,
758
+ !state.isDocked,
759
+ !isGeometryTransitioning else { return }
760
+
761
+ state.dockEdge = .right
762
+ tuckTimer = Timer.scheduledTimer(withTimeInterval: edgeTuckIdleInterval, repeats: false) { [weak self] _ in
763
+ self?.tuckBubble(animated: true)
764
+ }
765
+ }
766
+
767
+ private func suppressHoverUntilPointerMoves() {
768
+ hoverGate.suppress(at: NSEvent.mouseLocation)
769
+ cancelDwellTimer()
770
+ }
771
+
772
+ private func pointerMovementRearmedHover() -> Bool {
773
+ hoverGate.allowsHover(at: NSEvent.mouseLocation)
774
+ }
775
+
776
+ /// Compact docking is visual-only. Circle and pill share the same stationary
777
+ /// 76x76 host; no NSWindow frame is changed here.
778
+ public func tuckBubble(animated: Bool = true) {
779
+ guard !state.isExpanded,
780
+ !state.isPinned,
781
+ !isInteractingOrDragging,
782
+ !state.isDocked,
783
+ !isGeometryTransitioning else { return }
784
+
785
+ cancelTuckTimer()
786
+ state.dockEdge = .right
787
+ suppressHoverUntilPointerMoves()
788
+ state.isDocked = true
789
+ }
790
+
791
+ public func unTuckBubble(pointerLocationInHost: NSPoint? = nil, animated: Bool = true) {
792
+ cancelTuckTimer()
793
+ guard state.isDocked, !isGeometryTransitioning else { return }
794
+ state.dockEdge = .right
795
+ state.isDocked = false
796
+
797
+ if let pointerLocationInHost,
798
+ OverlayCompactHitRegion.contains(
799
+ pointerLocationInHost,
800
+ in: NSRect(origin: .zero, size: bubbleSize),
801
+ expanded: false,
802
+ docked: false
803
+ ),
804
+ !isInteractingOrDragging {
805
+ resetDwellTimer()
806
+ }
807
+ }
808
+
809
+ // MARK: - Hover-Dwell Detection
810
+ public func cancelDwellTimer() {
811
+ hoverDwellTimer?.invalidate()
812
+ hoverDwellTimer = nil
813
+ }
814
+
815
+ public func handleMouseEntered(at point: NSPoint) {
816
+ if isGeometryTransitioning {
817
+ needsPointerReconciliationAfterGeometry = true
818
+ return
819
+ }
820
+ guard pointerMovementRearmedHover() else { return }
821
+
822
+ cancelTuckTimer()
823
+ collapseTimer?.invalidate()
824
+ collapseTimer = nil
825
+
826
+ if state.isDocked {
827
+ unTuckBubble(pointerLocationInHost: point, animated: true)
828
+ return
829
+ }
830
+
831
+ guard !state.isExpanded, !isInteractingOrDragging else { return }
832
+ resetDwellTimer()
833
+ }
834
+
835
+ public func handleMouseMoved(at point: NSPoint) {
836
+ if isGeometryTransitioning {
837
+ needsPointerReconciliationAfterGeometry = true
838
+ return
839
+ }
840
+ guard pointerMovementRearmedHover() else { return }
841
+
842
+ if state.isDocked {
843
+ unTuckBubble(pointerLocationInHost: point, animated: true)
844
+ return
845
+ }
846
+
847
+ guard !state.isExpanded, !isInteractingOrDragging else { return }
848
+ resetDwellTimer()
849
+ }
850
+
851
+ private func resetDwellTimer() {
852
+ cancelDwellTimer()
853
+ guard !isInteractingOrDragging, !isGeometryTransitioning else { return }
854
+ hoverDwellTimer = Timer.scheduledTimer(withTimeInterval: 0.4, repeats: false) { [weak self] _ in
855
+ guard let self,
856
+ !self.state.isExpanded,
857
+ !self.isInteractingOrDragging,
858
+ !self.isGeometryTransitioning else { return }
859
+ self.state.expand()
860
+ }
861
+ }
862
+
863
+ public func handleMouseExited() {
864
+ cancelDwellTimer()
865
+ if isGeometryTransitioning {
866
+ needsPointerReconciliationAfterGeometry = true
867
+ return
868
+ }
869
+
870
+ if state.isExpanded && !state.isPinned && !isInteractingOrDragging {
871
+ scheduleAutoCollapseTimer()
872
+ } else if !state.isExpanded && !state.isDocked && !isInteractingOrDragging {
873
+ scheduleTuck()
874
+ }
875
+ }
876
+
877
+ private func scheduleAutoCollapseTimer() {
878
+ collapseTimer?.invalidate()
879
+ collapseTimer = Timer.scheduledTimer(withTimeInterval: 0.8, repeats: false) { [weak self] _ in
880
+ guard let self,
881
+ self.state.isExpanded,
882
+ !self.state.isPinned,
883
+ !self.isInteractingOrDragging,
884
+ !self.isGeometryTransitioning else { return }
885
+
886
+ if self.isPointerInAppWindowOrPopover() {
887
+ self.scheduleAutoCollapseTimer()
888
+ return
889
+ }
890
+
891
+ self.state.collapse()
892
+ }
893
+ }
894
+
895
+ private func isPointerInAppWindowOrPopover() -> Bool {
896
+ let mouseLocation = NSEvent.mouseLocation
897
+ for appWindow in NSApp.windows {
898
+ guard appWindow.isVisible,
899
+ appWindow.alphaValue > 0.01,
900
+ appWindow.frame.width > 10,
901
+ appWindow.frame.height > 10 else {
902
+ continue
903
+ }
904
+ if let window, appWindow == window {
905
+ if appWindow.frame.contains(mouseLocation),
906
+ let contentView = appWindow.contentView {
907
+ let pointInWindow = appWindow.convertPoint(fromScreen: mouseLocation)
908
+ let pointInHost = contentView.convert(pointInWindow, from: nil)
909
+ if isPointerInteractive(at: pointInHost, in: contentView.bounds) {
910
+ return true
911
+ }
912
+ }
913
+ } else {
914
+ if appWindow.frame.contains(mouseLocation) {
915
+ return true
916
+ }
917
+ }
918
+ }
919
+ return false
920
+ }
921
+
922
+ // MARK: - Drag Snap
923
+ func performMagneticSnap(for window: NSWindow, pointerLocation: NSPoint) {
924
+ guard runtime.beginSnapGeometry() else {
925
+ if runtime.claimPendingPresentationIfIdle() {
926
+ performPresentationFrameUpdate(animated: pendingPresentationAnimated)
927
+ }
928
+ return
929
+ }
930
+
931
+ guard let visible = resolvedVisibleFrame(for: window.frame, preferredPoint: pointerLocation) else {
932
+ let startedNext = finishGeometryActivity()
933
+ if !startedNext, !state.isExpanded {
934
+ scheduleTuck()
935
+ }
936
+ return
937
+ }
938
+
939
+ let frame = window.frame
940
+ var targetOrigin = frame.origin
941
+ let distLeft = abs(frame.minX - visible.minX)
942
+ let distRight = abs(visible.maxX - frame.maxX)
943
+ let distTop = abs(visible.maxY - frame.maxY)
944
+ let distBottom = abs(frame.minY - visible.minY)
945
+
946
+ if state.isExpanded {
947
+ if distLeft < snapThreshold {
948
+ targetOrigin.x = visible.minX + snapMargin
949
+ } else if distRight < snapThreshold {
950
+ targetOrigin.x = visible.maxX - frame.width - snapMargin
951
+ }
952
+ if distTop < snapThreshold {
953
+ targetOrigin.y = visible.maxY - frame.height - snapMargin
954
+ } else if distBottom < snapThreshold {
955
+ targetOrigin.y = visible.minY + snapMargin
956
+ }
957
+ targetOrigin = OverlayScreenGeometry.clamp(
958
+ targetOrigin,
959
+ windowSize: frame.size,
960
+ to: visible
961
+ )
962
+ } else {
963
+ state.dockEdge = .right
964
+ targetOrigin.x = visible.maxX - frame.width
965
+ if distTop < snapThreshold {
966
+ targetOrigin.y = visible.maxY - frame.height - snapMargin
967
+ } else if distBottom < snapThreshold {
968
+ targetOrigin.y = visible.minY + snapMargin
969
+ } else {
970
+ targetOrigin.y = max(
971
+ visible.minY + snapMargin,
972
+ min(targetOrigin.y, visible.maxY - frame.height - snapMargin)
973
+ )
974
+ }
975
+ }
976
+
977
+ let targetFrame = NSRect(origin: targetOrigin, size: frame.size)
978
+ suppressHoverUntilPointerMoves()
979
+
980
+ let completed: () -> Void = { [weak self] in
981
+ guard let self else { return }
982
+ self.window.orderFrontRegardless()
983
+ self.saveWindowPosition(targetOrigin)
984
+ let startedNext = self.finishGeometryActivity()
985
+ if !startedNext, !self.state.isExpanded {
986
+ self.scheduleTuck()
987
+ }
988
+ }
989
+
990
+ if approximatelyEqual(window.frame, targetFrame) {
991
+ window.setFrame(targetFrame, display: true)
992
+ completed()
993
+ return
994
+ }
995
+
996
+ NSAnimationContext.runAnimationGroup({ context in
997
+ context.duration = 0.16
998
+ context.timingFunction = CAMediaTimingFunction(name: .easeOut)
999
+ context.allowsImplicitAnimation = true
1000
+ window.animator().setFrame(targetFrame, display: true)
1001
+ }, completionHandler: completed)
1002
+ }
1003
+
1004
+ // MARK: - Position Persistence
1005
+ private let positionKey = "CodexFlowOverlayWindowPosition"
1006
+
1007
+ public func saveWindowPosition(_ origin: NSPoint) {
1008
+ let dict: [String: Double] = ["x": Double(origin.x), "y": Double(origin.y)]
1009
+ UserDefaults.standard.set(dict, forKey: positionKey)
1010
+ }
1011
+
1012
+ private func loadSavedPosition() -> NSRect? {
1013
+ guard let dict = UserDefaults.standard.dictionary(forKey: positionKey) as? [String: Double],
1014
+ let x = dict["x"],
1015
+ let y = dict["y"] else {
1016
+ return nil
1017
+ }
1018
+ return NSRect(x: CGFloat(x), y: CGFloat(y), width: bubbleSize.width, height: bubbleSize.height)
1019
+ }
1020
+
1021
+ private func normalizedCollapsedFrame(_ frame: NSRect) -> NSRect {
1022
+ let visible = OverlayScreenGeometry.presentationVisibleFrame(for: frame, among: visibleFrames)
1023
+ ?? NSScreen.main?.visibleFrame
1024
+ ?? NSRect(x: 0, y: 0, width: 1440, height: 900)
1025
+ let x = visible.maxX - bubbleSize.width
1026
+ let y = max(visible.minY, min(frame.origin.y, visible.maxY - bubbleSize.height))
1027
+ return NSRect(origin: NSPoint(x: x, y: y), size: bubbleSize)
1028
+ }
1029
+
1030
+ private func defaultPosition(for size: NSSize) -> NSRect {
1031
+ let screen = NSScreen.main?.visibleFrame ?? NSRect(x: 0, y: 0, width: 1440, height: 900)
1032
+ let x = screen.maxX - size.width
1033
+ let y = screen.maxY - size.height - 32
1034
+ return NSRect(x: x, y: y, width: size.width, height: size.height)
1035
+ }
1036
+
1037
+ public func refreshTelemetry() {
1038
+ DispatchQueue.global(qos: .userInitiated).async {
1039
+ let latest = TelemetryQueryEngine.shared.loadLatestRun()
1040
+ DispatchQueue.main.async {
1041
+ if let run = latest {
1042
+ self.state.update(run: run)
1043
+ } else {
1044
+ self.state.latestRun = nil
1045
+ self.state.isTaskRunning = false
1046
+ self.state.loadHistory()
1047
+ self.state.loadStats()
1048
+ }
1049
+ }
1050
+ }
1051
+ }
1052
+ }