From a7829df1b4d732b51eca68ef135635454e95f18a Mon Sep 17 00:00:00 2001
From: blessdyb
Date: Sat, 26 Sep 2026 20:09:29 -0700
Subject: [PATCH 1/2] =?UTF-8?q?Flowlight=200.8.1=20=E2=80=94=20stop=20work?=
=?UTF-8?q?ing=20when=20nobody=20is=20looking?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Two fixes to the same symptom: macOS listing Flowlight under "Using
Significant Energy" while it sat idle in the menu bar. Measured 6.3% of a
core at idle, 37% averaged over three and a half hours.
Occlusion, not view lifecycle. The live chart series and the per-app list
were gated on uiVisible, which was driven by SwiftUI's onAppear and
onDisappear. Those track the view's lifecycle, which says only that a
window exists — it stays "appeared" while the window is minimised, fully
covered by another app, or on a Space nobody is looking at. In all three
the chart was rebuilt every second for an audience of nobody. It now
follows NSWindow.occlusionState, which is the state that actually answers
"can anyone see this".
The counter became a set. Occlusion notifications are not guaranteed to
pair up: a window closed while already occluded, or one missed teardown,
leaks a count — and a leaked count pins visibility to true for the rest of
the session, silently restoring the cost the gate exists to remove. Keying
on window identity makes every update idempotent. WindowVisibility is its
own type so the state machine is testable without constructing a
TrafficMonitor, whose init opens the real traffic database.
SMAppService out of a @State default. SettingsView read "launch at login"
in a property's default value. That expression re-runs every time the view
struct is constructed, and Settings { } is rebuilt whenever the App body
is invalidated — which TrafficMonitor did every second through its
@Published properties. SMAppService.status is a synchronous XPC round trip
to smd, so this put blocking IPC on the main thread at 1 Hz for the life
of the process, whether or not Settings was ever opened. A sample caught
the whole chain: AppBodyAccessor.updateBody → Settings.init(content:) →
SettingsView.init → -[SMAppService status] → _xpc_pipe_routine.
It is now read in .task, once, when the pane appears.
470 tests pass, including five new ones covering the drift cases that a
counter could not survive.
Co-Authored-By: Claude Opus 5 (1M context)
---
Flowlight/App/TrafficMonitor.swift | 20 +++--
Flowlight/App/WindowVisibility.swift | 39 ++++++++++
Flowlight/UI/Components.swift | 59 +++++++++++++++
Flowlight/UI/ContentView.swift | 5 +-
Flowlight/UI/SettingsView.swift | 8 +-
FlowlightTests/WindowVisibilityTests.swift | 86 ++++++++++++++++++++++
project.yml | 4 +-
site/pages/releases.html | 22 +++++-
8 files changed, 226 insertions(+), 17 deletions(-)
create mode 100644 Flowlight/App/WindowVisibility.swift
create mode 100644 FlowlightTests/WindowVisibilityTests.swift
diff --git a/Flowlight/App/TrafficMonitor.swift b/Flowlight/App/TrafficMonitor.swift
index da911eb..5d8fb13 100644
--- a/Flowlight/App/TrafficMonitor.swift
+++ b/Flowlight/App/TrafficMonitor.swift
@@ -69,19 +69,17 @@ final class TrafficMonitor: ObservableObject {
private var source: TrafficSource?
private var timers: [Timer] = []
- /// How many Flowlight windows are on screen. With none, the menu bar only needs the current rates, so the live
- /// chart series and the per-app list aren't built at all.
- private var visibleWindows = 0
- var uiVisible: Bool { visibleWindows > 0 }
+ private var windows = WindowVisibility()
- func windowAppeared() { visibleWindows += 1 }
+ /// Whether anything on screen needs the live chart series and the per-app list. With nothing visible the menu
+ /// bar only needs the current rates, so neither is built at all.
+ var uiVisible: Bool { windows.isVisible }
- func windowDisappeared() {
- visibleWindows = max(0, visibleWindows - 1)
- if !uiVisible {
- liveSeries = []
- talkers = []
- }
+ func windowVisibility(_ window: AnyObject, isVisible: Bool) {
+ guard windows.update(window, isVisible: isVisible) == .becameHidden else { return }
+ // Nothing can see them, and they are rebuilt on the next tick once something can.
+ liveSeries = []
+ talkers = []
}
private let liveWindow = 120
diff --git a/Flowlight/App/WindowVisibility.swift b/Flowlight/App/WindowVisibility.swift
new file mode 100644
index 0000000..119afa6
--- /dev/null
+++ b/Flowlight/App/WindowVisibility.swift
@@ -0,0 +1,39 @@
+import Foundation
+
+/// Which windows are genuinely on screen.
+///
+/// The answer gates the expensive half of every tick — the live chart series and the per-app list — so getting it
+/// wrong is not a visual bug, it is a battery bug that nothing on screen reveals.
+///
+/// Membership follows `NSWindow.occlusionState` rather than SwiftUI's `onAppear`/`onDisappear`. The view lifecycle
+/// only says the window *exists*: it stays "appeared" while the window is minimised, fully covered by another
+/// app, or on a Space nobody is looking at. In all three the chart was still being rebuilt every second for an
+/// audience of nobody. Occlusion is the state that actually answers "can anyone see this".
+///
+/// A set rather than a counter, because occlusion notifications are not guaranteed to pair up. A window closed
+/// while already occluded, or one teardown missed, leaks a count — and a leaked count pins visibility to `true`
+/// for the rest of the session, silently restoring the cost this exists to remove. Keying on identity makes every
+/// update idempotent, so a repeat cannot drift the state.
+struct WindowVisibility {
+ /// What an update did to the *overall* state, which is all the caller acts on.
+ enum Change: Equatable {
+ case unchanged
+ case becameVisible
+ case becameHidden
+ }
+
+ private var visible: Set = []
+
+ var isVisible: Bool { !visible.isEmpty }
+
+ /// Record one window's visibility. Safe to call repeatedly with the same value.
+ @discardableResult
+ mutating func update(_ window: AnyObject, isVisible: Bool) -> Change {
+ let was = self.isVisible
+ let id = ObjectIdentifier(window)
+ if isVisible { visible.insert(id) } else { visible.remove(id) }
+ let now = self.isVisible
+ if was == now { return .unchanged }
+ return now ? .becameVisible : .becameHidden
+ }
+}
diff --git a/Flowlight/UI/Components.swift b/Flowlight/UI/Components.swift
index f9231a4..9136cfa 100644
--- a/Flowlight/UI/Components.swift
+++ b/Flowlight/UI/Components.swift
@@ -92,6 +92,65 @@ enum TrafficColors {
/// `setFrameAutosaveName` looks like the answer and quietly isn't: SwiftUI's `WindowGroup` owns the window's
/// restoration, so the name is rejected and nothing is ever written. Watching the window and storing the frame
/// is a few more lines and actually works.
+/// Reports whether the hosting window is actually on screen.
+///
+/// `onAppear`/`onDisappear` track the view's lifecycle, which says nothing about whether anyone can see the
+/// window: it stays "appeared" when the window is minimised, fully covered, or on another Space. AppKit answers
+/// the real question through `occlusionState`, and posts when it changes.
+struct WindowVisibilityReporter: NSViewRepresentable {
+ let report: (AnyObject, Bool) -> Void
+
+ func makeCoordinator() -> Coordinator { Coordinator(report: report) }
+
+ func makeNSView(context: Context) -> NSView {
+ let view = NSView()
+ // The view has no window until it is in the hierarchy.
+ DispatchQueue.main.async { context.coordinator.attach(to: view.window) }
+ return view
+ }
+
+ func updateNSView(_ view: NSView, context: Context) {
+ // A window can be reassigned — moved between scenes, or restored after a close.
+ if view.window !== context.coordinator.window {
+ DispatchQueue.main.async { context.coordinator.attach(to: view.window) }
+ }
+ }
+
+ static func dismantleNSView(_ view: NSView, coordinator: Coordinator) {
+ coordinator.detach()
+ }
+
+ final class Coordinator {
+ private(set) weak var window: NSWindow?
+ private let report: (AnyObject, Bool) -> Void
+ private var observer: NSObjectProtocol?
+
+ init(report: @escaping (AnyObject, Bool) -> Void) { self.report = report }
+
+ func attach(to window: NSWindow?) {
+ guard window !== self.window else { return }
+ detach()
+ guard let window else { return }
+ self.window = window
+ observer = NotificationCenter.default.addObserver(
+ forName: NSWindow.didChangeOcclusionStateNotification, object: window, queue: .main
+ ) { [weak self, weak window] _ in
+ guard let self, let window else { return }
+ self.report(window, window.occlusionState.contains(.visible))
+ }
+ report(window, window.occlusionState.contains(.visible))
+ }
+
+ /// Report gone before dropping the window, or a close while occluded leaves it counted forever.
+ func detach() {
+ if let observer { NotificationCenter.default.removeObserver(observer) }
+ observer = nil
+ if let window { report(window, false) }
+ window = nil
+ }
+ }
+}
+
struct WindowSizer: NSViewRepresentable {
private static let key = "window.mainFrame"
diff --git a/Flowlight/UI/ContentView.swift b/Flowlight/UI/ContentView.swift
index e19ac0b..34fc73c 100644
--- a/Flowlight/UI/ContentView.swift
+++ b/Flowlight/UI/ContentView.swift
@@ -87,8 +87,9 @@ struct ContentView: View {
.id(localization.revision)
.onReceive(NotificationCenter.default.publisher(for: .flowlightOpenUpdate)) { _ in openWindow(id: "update") }
.background(WindowSizer())
- .onAppear { monitor.windowAppeared() }
- .onDisappear { monitor.windowDisappeared() }
+ .background(WindowVisibilityReporter { window, visible in
+ monitor.windowVisibility(window, isVisible: visible)
+ })
.onChange(of: updater.showWindow) { _, show in
if show { openWindow(id: "update"); updater.showWindow = false }
}
diff --git a/Flowlight/UI/SettingsView.swift b/Flowlight/UI/SettingsView.swift
index 89fefb1..2bcf196 100644
--- a/Flowlight/UI/SettingsView.swift
+++ b/Flowlight/UI/SettingsView.swift
@@ -20,7 +20,12 @@ struct SettingsView: View {
@AppStorage(K.agentAway) private var agentAway = true
@AppStorage(K.agentAwayMinutes) private var agentAwayMinutes = 15.0
@AppStorage(AnomalySettings.Keys.backgroundOnly) private var backgroundOnly = false
- @State private var launchAtLogin = SMAppService.mainApp.status == .enabled
+ // Read once the pane is on screen, never in the property's default expression: that
+ // expression re-runs every time this struct is constructed, and `Settings { }` is
+ // rebuilt whenever the App body is invalidated — which TrafficMonitor does every
+ // second. SMAppService.status is a synchronous XPC round trip to smd, so the default
+ // form put blocking IPC on the main thread at 1 Hz for the life of the process.
+ @State private var launchAtLogin = false
@State private var loginItemError: String?
var body: some View {
@@ -29,6 +34,7 @@ struct SettingsView: View {
LanguageRow()
Toggle(L("Launch at login"), isOn: $launchAtLogin)
.onChange(of: launchAtLogin) { _, enabled in setLaunchAtLogin(enabled) }
+ .task { launchAtLogin = SMAppService.mainApp.status == .enabled }
if let loginItemError { Text(loginItemError).font(.caption).foregroundStyle(.red) }
Toggle(isOn: $backgroundOnly) {
VStack(alignment: .leading, spacing: 2) {
diff --git a/FlowlightTests/WindowVisibilityTests.swift b/FlowlightTests/WindowVisibilityTests.swift
new file mode 100644
index 0000000..e8eef05
--- /dev/null
+++ b/FlowlightTests/WindowVisibilityTests.swift
@@ -0,0 +1,86 @@
+import XCTest
+@testable import Flowlight
+
+/// `WindowVisibility` decides whether the live chart series and the per-app list are built at all, so it is the
+/// switch between an idle Flowlight costing nothing and an idle Flowlight rebuilding both every second for a
+/// window nobody can see. A stuck `true` is invisible from the outside — the app keeps working correctly and only
+/// the battery shows it — so the drift cases are covered explicitly.
+final class WindowVisibilityTests: XCTestCase {
+ /// Stand-in for an NSWindow. Only identity is ever used.
+ private final class FakeWindow {}
+
+ func testNothingIsVisibleUntilAWindowSaysSo() {
+ var windows = WindowVisibility()
+ XCTAssertFalse(windows.isVisible)
+
+ let window = FakeWindow()
+ XCTAssertEqual(windows.update(window, isVisible: true), .becameVisible)
+ XCTAssertTrue(windows.isVisible)
+
+ XCTAssertEqual(windows.update(window, isVisible: false), .becameHidden)
+ XCTAssertFalse(windows.isVisible)
+ }
+
+ /// AppKit posts occlusion changes freely, including ones that repeat the current state. A counter drifted
+ /// here; identity cannot.
+ func testRepeatedUpdatesForOneWindowDoNotDrift() {
+ var windows = WindowVisibility()
+ let window = FakeWindow()
+
+ XCTAssertEqual(windows.update(window, isVisible: true), .becameVisible)
+ for _ in 0..<5 {
+ XCTAssertEqual(windows.update(window, isVisible: true), .unchanged, "a repeat is not a new window")
+ }
+
+ XCTAssertEqual(windows.update(window, isVisible: false), .becameHidden,
+ "one hide must undo any number of shows for the same window")
+ for _ in 0..<3 {
+ XCTAssertEqual(windows.update(window, isVisible: false), .unchanged)
+ }
+ XCTAssertFalse(windows.isVisible)
+ }
+
+ func testVisibleWhileAnyWindowIsVisible() {
+ var windows = WindowVisibility()
+ let first = FakeWindow()
+ let second = FakeWindow()
+
+ XCTAssertEqual(windows.update(first, isVisible: true), .becameVisible)
+ XCTAssertEqual(windows.update(second, isVisible: true), .unchanged, "already visible overall")
+
+ XCTAssertEqual(windows.update(first, isVisible: false), .unchanged, "the second is still on screen")
+ XCTAssertTrue(windows.isVisible)
+
+ XCTAssertEqual(windows.update(second, isVisible: false), .becameHidden)
+ XCTAssertFalse(windows.isVisible)
+ }
+
+ /// A window torn down while already occluded reports hidden twice. Under the old counter that double
+ /// decrement was clamped, and the matching double increment was not — which is how a session ended up
+ /// permanently "visible".
+ func testHidingAWindowThatWasNeverVisibleIsHarmless() {
+ var windows = WindowVisibility()
+ let ghost = FakeWindow()
+ let real = FakeWindow()
+
+ XCTAssertEqual(windows.update(ghost, isVisible: false), .unchanged)
+ XCTAssertFalse(windows.isVisible)
+
+ XCTAssertEqual(windows.update(real, isVisible: true), .becameVisible)
+ XCTAssertEqual(windows.update(ghost, isVisible: false), .unchanged, "a stranger cannot hide someone else")
+ XCTAssertTrue(windows.isVisible)
+ }
+
+ /// Two windows closing in either order must both end hidden; only the last one reports the flip.
+ func testOnlyTheLastWindowReportsTheFlip() {
+ var windows = WindowVisibility()
+ let a = FakeWindow()
+ let b = FakeWindow()
+ windows.update(a, isVisible: true)
+ windows.update(b, isVisible: true)
+
+ XCTAssertEqual(windows.update(b, isVisible: false), .unchanged)
+ XCTAssertEqual(windows.update(a, isVisible: false), .becameHidden)
+ XCTAssertFalse(windows.isVisible)
+ }
+}
diff --git a/project.yml b/project.yml
index fa71f06..0e04614 100644
--- a/project.yml
+++ b/project.yml
@@ -11,8 +11,8 @@ settings:
DEVELOPMENT_TEAM: ""
CODE_SIGN_STYLE: Automatic
ENABLE_HARDENED_RUNTIME: YES
- MARKETING_VERSION: "0.8.0"
- CURRENT_PROJECT_VERSION: "20"
+ MARKETING_VERSION: "0.8.1"
+ CURRENT_PROJECT_VERSION: "21"
targets:
Flowlight:
type: application
diff --git a/site/pages/releases.html b/site/pages/releases.html
index f24d7ec..53352c7 100644
--- a/site/pages/releases.html
+++ b/site/pages/releases.html
@@ -13,7 +13,27 @@
What's new
-
0.8.0
Latest
+
0.8.1
Latest
+
+
Flowlight stops working when you are not looking at it. It was rebuilding the live
+ chart and the per-app list every second whenever a window existed — including when that window was
+ minimised, completely covered by another app, or parked on a Space you had left. The work was gated on
+ SwiftUI's onAppear, which only says a window exists, not that anyone can see it. It now
+ follows the window's occlusion state, so a hidden Flowlight builds nothing and the menu bar rates keep
+ updating as before.
+
A blocking system call no longer ran once a second, forever. Settings read "launch
+ at login" in a property's default value, and that expression re-runs every time the settings pane is
+ constructed — which happened on every traffic update, whether or not Settings was open. Reading it
+ means a synchronous round trip to the system service manager on the main thread. It is now read once,
+ when the pane actually appears.
+
Together these are the difference between an idle Flowlight costing a measurable share of a CPU core
+ and costing almost nothing. If macOS had been listing Flowlight under "Using Significant Energy" while
+ it sat in the background, this is why.
+
+
+
+
+
0.8.0
Send an agent through the proxy in one click. HTTPS inspection can only read what
goes through Flowlight, and a program reads its proxy settings once — when it starts. So an agent
From 3cf7bf650346ce3931d62a4da8b931ce2b80b4d6 Mon Sep 17 00:00:00 2001
From: blessdyb
Date: Sun, 27 Sep 2026 00:32:26 -0700
Subject: [PATCH 2/2] Rebuild the site for 0.8.1
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The version bump landed without regenerating `docs/`, so the branch carries a site that still says 0.8.0 —
the version string, the structured data and the download button, in all ten languages — while `project.yml`
says 0.8.1. That is what the `site` check has been failing on since the branch was pushed.
It is the safe direction of the two (the site behind the release rather than ahead of it), but it is the
reason `docs/` and `site/` are checked against each other at all: they travel in one commit, because
`build_site.py` reads `MARKETING_VERSION`.
Co-Authored-By: Claude Opus 5 (1M context)
---
docs/404.html | 2 +-
docs/about/index.html | 2 +-
docs/de/about/index.html | 2 +-
docs/de/docs/index.html | 4 ++--
docs/de/index.html | 6 +++---
docs/de/privacy/index.html | 2 +-
docs/docs/index.html | 4 ++--
docs/es/about/index.html | 2 +-
docs/es/docs/index.html | 4 ++--
docs/es/index.html | 6 +++---
docs/es/privacy/index.html | 2 +-
docs/fr/about/index.html | 2 +-
docs/fr/docs/index.html | 4 ++--
docs/fr/index.html | 6 +++---
docs/fr/privacy/index.html | 2 +-
docs/index.html | 6 +++---
docs/it/about/index.html | 2 +-
docs/it/docs/index.html | 4 ++--
docs/it/index.html | 6 +++---
docs/it/privacy/index.html | 2 +-
docs/ja/about/index.html | 2 +-
docs/ja/docs/index.html | 4 ++--
docs/ja/index.html | 6 +++---
docs/ja/privacy/index.html | 2 +-
docs/ko/about/index.html | 2 +-
docs/ko/docs/index.html | 4 ++--
docs/ko/index.html | 6 +++---
docs/ko/privacy/index.html | 2 +-
docs/llms-full.txt | 26 +++++++++++++++++++++++---
docs/llms.txt | 2 +-
docs/privacy/index.html | 2 +-
docs/pt-PT/about/index.html | 2 +-
docs/pt-PT/docs/index.html | 4 ++--
docs/pt-PT/index.html | 6 +++---
docs/pt-PT/privacy/index.html | 2 +-
docs/releases/index.html | 24 ++++++++++++++++++++++--
docs/zh-Hans/about/index.html | 2 +-
docs/zh-Hans/docs/index.html | 4 ++--
docs/zh-Hans/index.html | 6 +++---
docs/zh-Hans/privacy/index.html | 2 +-
docs/zh-Hant/about/index.html | 2 +-
docs/zh-Hant/docs/index.html | 4 ++--
docs/zh-Hant/index.html | 6 +++---
docs/zh-Hant/privacy/index.html | 2 +-
44 files changed, 117 insertions(+), 77 deletions(-)
diff --git a/docs/404.html b/docs/404.html
index 112767e..494739f 100644
--- a/docs/404.html
+++ b/docs/404.html
@@ -81,7 +81,7 @@
diff --git a/docs/llms-full.txt b/docs/llms-full.txt
index 1a617a4..ff2d7d9 100644
--- a/docs/llms-full.txt
+++ b/docs/llms-full.txt
@@ -62,7 +62,7 @@ Documentation
Using Flowlight
- Everything from the first launch to tuning the agent rules. Flowlight 0.8.0, macOS 15 or later.
+ Everything from the first launch to tuning the agent rules. Flowlight 0.8.1, macOS 15 or later.
On this page
@@ -698,7 +698,7 @@ Understand your AI agents.
brew install --cask xinbetween/tap/flowlightCopy
- v0.8.0macOS 15+UniversalGPL-3.0No telemetry
+ v0.8.1macOS 15+UniversalGPL-3.0No telemetry
connectionslive
@@ -1244,9 +1244,29 @@ Releases
Downloads, checksums and full notes for each version are on GitHub Releases.
- 0.8.0
+ 0.8.1
September 26, 2026Latest
+ Flowlight stops working when you are not looking at it. It was rebuilding the live
+ chart and the per-app list every second whenever a window existed — including when that window was
+ minimised, completely covered by another app, or parked on a Space you had left. The work was gated on
+ SwiftUI's onAppear, which only says a window exists, not that anyone can see it. It now
+ follows the window's occlusion state, so a hidden Flowlight builds nothing and the menu bar rates keep
+ updating as before.
+
+ A blocking system call no longer ran once a second, forever. Settings read "launch
+ at login" in a property's default value, and that expression re-runs every time the settings pane is
+ constructed — which happened on every traffic update, whether or not Settings was open. Reading it
+ means a synchronous round trip to the system service manager on the main thread. It is now read once,
+ when the pane actually appears.
+
+ Together these are the difference between an idle Flowlight costing a measurable share of a CPU core
+ and costing almost nothing. If macOS had been listing Flowlight under "Using Significant Energy" while
+ it sat in the background, this is why.
+
+ 0.8.0
+September 26, 2026
+
Send an agent through the proxy in one click. HTTPS inspection can only read what
goes through Flowlight, and a program reads its proxy settings once — when it starts. So an agent
already running when you switched inspection on sends its model traffic straight past it, and its
diff --git a/docs/llms.txt b/docs/llms.txt
index b80ddba..20477c5 100644
--- a/docs/llms.txt
+++ b/docs/llms.txt
@@ -1,6 +1,6 @@
# Flowlight
-> Free, open-source (GPL-3.0) application-aware network monitor for macOS, with focused visibility into AI agents. It attributes observed TCP and UDP activity to the application that made it and records the destination, protocol and byte counts, keeping local history from second to year. Recognized AI agents are listed by name along with the tools and MCP servers they start, under per-agent allowlists. It can also refuse, once asked: a rule blocks an application, a destination or a URL for as long as you specify, and a guardrail withholds a tool from an agent before its model is offered it. Runs on macOS 15 or later; capture is by a sampler or a Network Extension. Current version: 0.8.0.
+> Free, open-source (GPL-3.0) application-aware network monitor for macOS, with focused visibility into AI agents. It attributes observed TCP and UDP activity to the application that made it and records the destination, protocol and byte counts, keeping local history from second to year. Recognized AI agents are listed by name along with the tools and MCP servers they start, under per-agent allowlists. It can also refuse, once asked: a rule blocks an application, a destination or a URL for as long as you specify, and a guardrail withholds a tool from an agent before its model is offered it. Runs on macOS 15 or later; capture is by a sampler or a Network Extension. Current version: 0.8.1.
- [Download Flowlight.dmg](https://github.com/xinbetween/flowlight/releases/latest/download/Flowlight.dmg)
- [Source code](https://github.com/xinbetween/flowlight)
diff --git a/docs/privacy/index.html b/docs/privacy/index.html
index ac6b7b6..5e5e485 100644
--- a/docs/privacy/index.html
+++ b/docs/privacy/index.html
@@ -169,7 +169,7 @@
Flowlight stops working when you are not looking at it. It was rebuilding the live
+ chart and the per-app list every second whenever a window existed — including when that window was
+ minimised, completely covered by another app, or parked on a Space you had left. The work was gated on
+ SwiftUI's onAppear, which only says a window exists, not that anyone can see it. It now
+ follows the window's occlusion state, so a hidden Flowlight builds nothing and the menu bar rates keep
+ updating as before.
+
A blocking system call no longer ran once a second, forever. Settings read "launch
+ at login" in a property's default value, and that expression re-runs every time the settings pane is
+ constructed — which happened on every traffic update, whether or not Settings was open. Reading it
+ means a synchronous round trip to the system service manager on the main thread. It is now read once,
+ when the pane actually appears.
+
Together these are the difference between an idle Flowlight costing a measurable share of a CPU core
+ and costing almost nothing. If macOS had been listing Flowlight under "Using Significant Energy" while
+ it sat in the background, this is why.
+
+
+
+
+
0.8.0
Send an agent through the proxy in one click. HTTPS inspection can only read what
goes through Flowlight, and a program reads its proxy settings once — when it starts. So an agent
@@ -714,7 +734,7 @@