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 @@

    That page isn't here

    Try the home page,

diff --git a/docs/about/index.html b/docs/about/index.html index 0965763..5ad8649 100644 --- a/docs/about/index.html +++ b/docs/about/index.html @@ -139,7 +139,7 @@

Thanks

diff --git a/docs/de/about/index.html b/docs/de/about/index.html index 1ac3ce7..b79a22b 100644 --- a/docs/de/about/index.html +++ b/docs/de/about/index.html @@ -138,7 +138,7 @@

Dank

diff --git a/docs/de/docs/index.html b/docs/de/docs/index.html index 182472a..f54e210 100644 --- a/docs/de/docs/index.html +++ b/docs/de/docs/index.html @@ -62,7 +62,7 @@

Dokumentation

Flowlight benutzen

-

Alles vom ersten Start bis zum Feinschliff an den Agentenregeln. Flowlight 0.8.0, macOS 15 oder neuer.

+

Alles vom ersten Start bis zum Feinschliff an den Agentenregeln. Flowlight 0.8.1, macOS 15 oder neuer.

diff --git a/docs/de/index.html b/docs/de/index.html index 6fd0e97..fd0e08a 100644 --- a/docs/de/index.html +++ b/docs/de/index.html @@ -36,7 +36,7 @@ - + @@ -76,7 +76,7 @@

Sieh, was deine Apps im Netz tun.
brew install --cask xinbetween/tap/flowlight

-

v0.8.0macOS 15+UniversalGPL-3.0Keine Telemetrie

+

v0.8.1macOS 15+UniversalGPL-3.0Keine Telemetrie

@@ -613,7 +613,7 @@

Wisse, was deinen Mac verlässt.

diff --git a/docs/de/privacy/index.html b/docs/de/privacy/index.html index 705489f..d15556d 100644 --- a/docs/de/privacy/index.html +++ b/docs/de/privacy/index.html @@ -168,7 +168,7 @@

Kontakt

diff --git a/docs/docs/index.html b/docs/docs/index.html index 7fabb1f..5512051 100644 --- a/docs/docs/index.html +++ b/docs/docs/index.html @@ -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.

diff --git a/docs/es/about/index.html b/docs/es/about/index.html index f3d99f7..61586b9 100644 --- a/docs/es/about/index.html +++ b/docs/es/about/index.html @@ -138,7 +138,7 @@

Agradecimientos

diff --git a/docs/es/docs/index.html b/docs/es/docs/index.html index 790b6c3..623e830 100644 --- a/docs/es/docs/index.html +++ b/docs/es/docs/index.html @@ -62,7 +62,7 @@

Documentación

Usar Flowlight

-

Todo, desde el primer arranque hasta el ajuste de las reglas de agentes. Flowlight 0.8.0, macOS 15 o posterior.

+

Todo, desde el primer arranque hasta el ajuste de las reglas de agentes. Flowlight 0.8.1, macOS 15 o posterior.

diff --git a/docs/es/index.html b/docs/es/index.html index 5b34fa1..b0f9026 100644 --- a/docs/es/index.html +++ b/docs/es/index.html @@ -36,7 +36,7 @@ - + @@ -76,7 +76,7 @@

Observa la actividad de red de tus apps.

brew install --cask xinbetween/tap/flowlight

-

v0.8.0macOS 15+UniversalGPL-3.0Sin telemetría

+

v0.8.1macOS 15+UniversalGPL-3.0Sin telemetría

@@ -613,7 +613,7 @@

Ten claro qué sale de tu Mac.

diff --git a/docs/es/privacy/index.html b/docs/es/privacy/index.html index 8af7f5b..94bfba0 100644 --- a/docs/es/privacy/index.html +++ b/docs/es/privacy/index.html @@ -168,7 +168,7 @@

Contacto

diff --git a/docs/fr/about/index.html b/docs/fr/about/index.html index 6898ada..ae4e0b5 100644 --- a/docs/fr/about/index.html +++ b/docs/fr/about/index.html @@ -138,7 +138,7 @@

Remerciements

diff --git a/docs/fr/docs/index.html b/docs/fr/docs/index.html index 2ee2363..09d6d62 100644 --- a/docs/fr/docs/index.html +++ b/docs/fr/docs/index.html @@ -62,7 +62,7 @@

Documentation

Utiliser Flowlight

-

Tout, du premier lancement au réglage des règles des agents. Flowlight 0.8.0, macOS 15 ou version ultérieure.

+

Tout, du premier lancement au réglage des règles des agents. Flowlight 0.8.1, macOS 15 ou version ultérieure.

diff --git a/docs/fr/index.html b/docs/fr/index.html index 33110ef..09190d6 100644 --- a/docs/fr/index.html +++ b/docs/fr/index.html @@ -36,7 +36,7 @@ - + @@ -76,7 +76,7 @@

Voyez l’activité réseau de vos apps.

brew install --cask xinbetween/tap/flowlight

-

v0.8.0macOS 15+UniverselGPL-3.0Sans télémétrie

+

v0.8.1macOS 15+UniverselGPL-3.0Sans télémétrie

@@ -613,7 +613,7 @@

Sachez ce qui quitte votre Mac.

diff --git a/docs/fr/privacy/index.html b/docs/fr/privacy/index.html index 31bba18..60fc694 100644 --- a/docs/fr/privacy/index.html +++ b/docs/fr/privacy/index.html @@ -168,7 +168,7 @@

Contact

diff --git a/docs/index.html b/docs/index.html index 4d9c97e..95a3683 100644 --- a/docs/index.html +++ b/docs/index.html @@ -36,7 +36,7 @@ - + @@ -76,7 +76,7 @@

See your apps' network activity.
brew install --cask xinbetween/tap/flowlight

-

v0.8.0macOS 15+UniversalGPL-3.0No telemetry

+

v0.8.1macOS 15+UniversalGPL-3.0No telemetry

@@ -614,7 +614,7 @@

Know what leaves your Mac.

diff --git a/docs/it/about/index.html b/docs/it/about/index.html index cdc7b4a..f519c3c 100644 --- a/docs/it/about/index.html +++ b/docs/it/about/index.html @@ -138,7 +138,7 @@

Ringraziamenti

diff --git a/docs/it/docs/index.html b/docs/it/docs/index.html index fcdaddc..f890837 100644 --- a/docs/it/docs/index.html +++ b/docs/it/docs/index.html @@ -62,7 +62,7 @@

Documentazione

Usare Flowlight

-

Tutto, dal primo avvio alla messa a punto delle regole per gli agenti. Flowlight 0.8.0, macOS 15 o successivo.

+

Tutto, dal primo avvio alla messa a punto delle regole per gli agenti. Flowlight 0.8.1, macOS 15 o successivo.

diff --git a/docs/it/index.html b/docs/it/index.html index 8611f7a..4e3dc20 100644 --- a/docs/it/index.html +++ b/docs/it/index.html @@ -36,7 +36,7 @@ - + @@ -76,7 +76,7 @@

Vedi la rete delle tue app.
Ca Metti una stella su GitHub

brew install --cask xinbetween/tap/flowlight

-

v0.8.0macOS 15+UniversaleGPL-3.0Nessuna telemetria

+

v0.8.1macOS 15+UniversaleGPL-3.0Nessuna telemetria

@@ -613,7 +613,7 @@

Sappi che cosa esce dal tuo Mac.

diff --git a/docs/it/privacy/index.html b/docs/it/privacy/index.html index c2f9355..2ec7e71 100644 --- a/docs/it/privacy/index.html +++ b/docs/it/privacy/index.html @@ -168,7 +168,7 @@

Contatti

diff --git a/docs/ja/about/index.html b/docs/ja/about/index.html index af81e46..1bf4d93 100644 --- a/docs/ja/about/index.html +++ b/docs/ja/about/index.html @@ -139,7 +139,7 @@

謝辞

diff --git a/docs/ja/docs/index.html b/docs/ja/docs/index.html index e0a08d0..7571189 100644 --- a/docs/ja/docs/index.html +++ b/docs/ja/docs/index.html @@ -62,7 +62,7 @@

ドキュメント

Flowlight の使い方

-

初回起動からエージェントのルールの調整まで、すべてここに。Flowlight 0.8.0、macOS 15 以降。

+

初回起動からエージェントのルールの調整まで、すべてここに。Flowlight 0.8.1、macOS 15 以降。

diff --git a/docs/ja/index.html b/docs/ja/index.html index bb22e40..08c9e59 100644 --- a/docs/ja/index.html +++ b/docs/ja/index.html @@ -36,7 +36,7 @@ - + @@ -76,7 +76,7 @@

アプリの通信が見える。
brew install --cask xinbetween/tap/flowlight

-

v0.8.0macOS 15+ユニバーサルGPL-3.0テレメトリなし

+

v0.8.1macOS 15+ユニバーサルGPL-3.0テレメトリなし

@@ -612,7 +612,7 @@

Mac から何が出ていくのかを知る。

diff --git a/docs/ja/privacy/index.html b/docs/ja/privacy/index.html index 7f846bc..840041e 100644 --- a/docs/ja/privacy/index.html +++ b/docs/ja/privacy/index.html @@ -167,7 +167,7 @@

連絡先

diff --git a/docs/ko/about/index.html b/docs/ko/about/index.html index b266cce..1e2953d 100644 --- a/docs/ko/about/index.html +++ b/docs/ko/about/index.html @@ -139,7 +139,7 @@

감사

diff --git a/docs/ko/docs/index.html b/docs/ko/docs/index.html index abc6757..827afd4 100644 --- a/docs/ko/docs/index.html +++ b/docs/ko/docs/index.html @@ -62,7 +62,7 @@

문서

Flowlight 사용하기

-

첫 실행부터 에이전트 규칙 조정까지 전부. Flowlight 0.8.0, macOS 15 이상.

+

첫 실행부터 에이전트 규칙 조정까지 전부. Flowlight 0.8.1, macOS 15 이상.

diff --git a/docs/ko/index.html b/docs/ko/index.html index c2f59c3..2e49306 100644 --- a/docs/ko/index.html +++ b/docs/ko/index.html @@ -36,7 +36,7 @@ - + @@ -76,7 +76,7 @@

앱이 무엇을 하는지 봅니다.

brew install --cask xinbetween/tap/flowlight

-

v0.8.0macOS 15+유니버설GPL-3.0텔레메트리 없음

+

v0.8.1macOS 15+유니버설GPL-3.0텔레메트리 없음

@@ -613,7 +613,7 @@

내 Mac에서 무엇이 나가는지 아세요.

diff --git a/docs/ko/privacy/index.html b/docs/ko/privacy/index.html index 699f588..44099ad 100644 --- a/docs/ko/privacy/index.html +++ b/docs/ko/privacy/index.html @@ -170,7 +170,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 @@

Contact

diff --git a/docs/pt-PT/about/index.html b/docs/pt-PT/about/index.html index c44295f..29179f4 100644 --- a/docs/pt-PT/about/index.html +++ b/docs/pt-PT/about/index.html @@ -138,7 +138,7 @@

Agradecimentos

diff --git a/docs/pt-PT/docs/index.html b/docs/pt-PT/docs/index.html index 73caef8..8c67b06 100644 --- a/docs/pt-PT/docs/index.html +++ b/docs/pt-PT/docs/index.html @@ -62,7 +62,7 @@

Documentação

Usar o Flowlight

-

Tudo, da primeira abertura ao ajuste das regras dos agentes. Flowlight 0.8.0, macOS 15 ou posterior.

+

Tudo, da primeira abertura ao ajuste das regras dos agentes. Flowlight 0.8.1, macOS 15 ou posterior.

diff --git a/docs/pt-PT/index.html b/docs/pt-PT/index.html index 92df4a5..ade2211 100644 --- a/docs/pt-PT/index.html +++ b/docs/pt-PT/index.html @@ -36,7 +36,7 @@ - + @@ -76,7 +76,7 @@

Veja a atividade de rede das suas apps.

brew install --cask xinbetween/tap/flowlight

-

v0.8.0macOS 15+UniversalGPL-3.0Sem telemetria

+

v0.8.1macOS 15+UniversalGPL-3.0Sem telemetria

@@ -613,7 +613,7 @@

Saiba o que sai do seu Mac.

diff --git a/docs/pt-PT/privacy/index.html b/docs/pt-PT/privacy/index.html index 06c2091..d5066ce 100644 --- a/docs/pt-PT/privacy/index.html +++ b/docs/pt-PT/privacy/index.html @@ -168,7 +168,7 @@

Contacto

diff --git a/docs/releases/index.html b/docs/releases/index.html index 30c393a..aff1e91 100644 --- a/docs/releases/index.html +++ b/docs/releases/index.html @@ -56,7 +56,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 @@ -714,7 +734,7 @@

    What's new

diff --git a/docs/zh-Hans/about/index.html b/docs/zh-Hans/about/index.html index cbba15a..3a8543b 100644 --- a/docs/zh-Hans/about/index.html +++ b/docs/zh-Hans/about/index.html @@ -138,7 +138,7 @@

致谢

diff --git a/docs/zh-Hans/docs/index.html b/docs/zh-Hans/docs/index.html index ed225f9..60c4d5e 100644 --- a/docs/zh-Hans/docs/index.html +++ b/docs/zh-Hans/docs/index.html @@ -62,7 +62,7 @@

文档

使用 Flowlight

-

从第一次启动到调整代理规则,需要的都在这里。Flowlight 0.8.0,macOS 15 或更高版本。

+

从第一次启动到调整代理规则,需要的都在这里。Flowlight 0.8.1,macOS 15 或更高版本。

diff --git a/docs/zh-Hans/index.html b/docs/zh-Hans/index.html index dcf36e8..bf5196e 100644 --- a/docs/zh-Hans/index.html +++ b/docs/zh-Hans/index.html @@ -36,7 +36,7 @@ - + @@ -76,7 +76,7 @@

看清应用的网络活动。

brew install --cask xinbetween/tap/flowlight

-

v0.8.0macOS 15+通用架构GPL-3.0无遥测

+

v0.8.1macOS 15+通用架构GPL-3.0无遥测

@@ -611,7 +611,7 @@

知道什么离开了你的 Mac。

diff --git a/docs/zh-Hans/privacy/index.html b/docs/zh-Hans/privacy/index.html index 526f469..260d0b4 100644 --- a/docs/zh-Hans/privacy/index.html +++ b/docs/zh-Hans/privacy/index.html @@ -168,7 +168,7 @@

联系方式

diff --git a/docs/zh-Hant/about/index.html b/docs/zh-Hant/about/index.html index 53d4cde..9a6a4ad 100644 --- a/docs/zh-Hant/about/index.html +++ b/docs/zh-Hant/about/index.html @@ -138,7 +138,7 @@

致謝

diff --git a/docs/zh-Hant/docs/index.html b/docs/zh-Hant/docs/index.html index 14b071e..5362e32 100644 --- a/docs/zh-Hant/docs/index.html +++ b/docs/zh-Hant/docs/index.html @@ -62,7 +62,7 @@

說明文件

使用 Flowlight

-

從第一次啟動到調整代理規則,全都在這裡。Flowlight 0.8.0,macOS 15 或以上版本。

+

從第一次啟動到調整代理規則,全都在這裡。Flowlight 0.8.1,macOS 15 或以上版本。

diff --git a/docs/zh-Hant/index.html b/docs/zh-Hant/index.html index bef192b..c9d957b 100644 --- a/docs/zh-Hant/index.html +++ b/docs/zh-Hant/index.html @@ -36,7 +36,7 @@ - + @@ -76,7 +76,7 @@

看見每個 App 的網路活動。
brew install --cask xinbetween/tap/flowlight

-

v0.8.0macOS 15+通用架構GPL-3.0無遙測

+

v0.8.1macOS 15+通用架構GPL-3.0無遙測

@@ -612,7 +612,7 @@

知道有什麼離開了你的 Mac。

diff --git a/docs/zh-Hant/privacy/index.html b/docs/zh-Hant/privacy/index.html index fe9036c..22c4f41 100644 --- a/docs/zh-Hant/privacy/index.html +++ b/docs/zh-Hant/privacy/index.html @@ -168,7 +168,7 @@

聯絡