From 554dac3edf148a0fadfd342afb33073e276806cd Mon Sep 17 00:00:00 2001 From: Nick Lemaire Date: Tue, 21 Apr 2026 10:00:00 +0200 Subject: [PATCH] Hide overlay when no sessions are active --- AgentGlance/App/AppState.swift | 50 +++++++++++++++++++++---- AgentGlance/Utilities/Constants.swift | 1 + AgentGlance/Utilities/NotchWindow.swift | 4 ++ AgentGlance/Views/SettingsView.swift | 2 + 4 files changed, 50 insertions(+), 7 deletions(-) diff --git a/AgentGlance/App/AppState.swift b/AgentGlance/App/AppState.swift index 44ebdf2..c06eead 100644 --- a/AgentGlance/App/AppState.swift +++ b/AgentGlance/App/AppState.swift @@ -32,6 +32,7 @@ final class AppState { private var localKeyMonitor: Any? private var appearanceObserver: AnyCancellable? private var screenModeObserver: AnyCancellable? + private var hideOverlayObserver: AnyCancellable? /// Posted when the notch should auto-expand (e.g. approval came in) var shouldAutoExpand = false @@ -123,6 +124,7 @@ final class AppState { registerGlobalHotkey() observeAppearanceChanges() observeScreenModeChanges() + observeHideOverlayChanges() sessionManager.startLivenessChecks() // Start web remote server if enabled @@ -141,11 +143,16 @@ final class AppState { // Prompt to move to /Applications if running from elsewhere checkAppLocation() + + // Apply initial visibility — classic mode's notchWindow exists synchronously. + // System chrome mode is handled via a delayed call inside createNotchWindow. + updateNotchVisibility() } private func setupBindings() { hookServer.onEvent = { [weak self] payload in self?.sessionManager.handleEvent(payload) + self?.updateNotchVisibility() // Broadcast session update to web remote clients if let self, let session = self.sessionManager.sessions[payload.session_id] { self.webRemoteServer?.broadcastSessionUpdate(session) @@ -353,11 +360,27 @@ final class AppState { // MARK: - Notch Window private func updateNotchVisibility() { - if notchWindow == nil { - createNotchWindow() + let hideWhenEmpty = UserDefaults.standard.bool( + forKey: Constants.UserDefaultsKeys.hideOverlayWhenEmpty + ) + let shouldHide = hideWhenEmpty && sessionManager.activeSessions.isEmpty + + guard let window = overlayWindow() else { return } + if shouldHide { + window.orderOut(nil) + } else { + window.orderFront(nil) + } + } + + /// Returns the overlay window regardless of window mode. + /// Classic mode uses the directly-managed `notchWindow`; system chrome mode + /// looks up the SwiftUI WindowGroup's window by identifier prefix. + private func overlayWindow() -> NSWindow? { + if let notchWindow { return notchWindow } + return NSApplication.shared.windows.first { + $0.identifier?.rawValue.contains("system-chrome") == true } - // Always keep notch visible — it shows/hides content based on sessions - notchWindow?.orderFront(nil) } private func createNotchWindow() { @@ -370,6 +393,11 @@ final class AppState { DispatchQueue.main.asyncAfter(deadline: .now() + 1) { NotificationCenter.default.post(name: .openSystemChromeWindow, object: nil) } + // Apply hideOverlayWhenEmpty once the SwiftUI scene has had a chance + // to open its window (brief ~100ms pop is acceptable per spec). + DispatchQueue.main.asyncAfter(deadline: .now() + 1.1) { [weak self] in + self?.updateNotchVisibility() + } return } // Always close the old window first — NSPanels stay visible @@ -383,9 +411,7 @@ final class AppState { func refreshNotchWindow() { notchWindow?.close() notchWindow = nil - if !sessionManager.activeSessions.isEmpty { - createNotchWindow() - } + createNotchWindow() } // MARK: - Testing @@ -833,6 +859,16 @@ final class AppState { } } + private func observeHideOverlayChanges() { + hideOverlayObserver = UserDefaults.standard.publisher( + for: \.hideOverlayWhenEmpty + ).sink { [weak self] _ in + Task { @MainActor [weak self] in + self?.updateNotchVisibility() + } + } + } + func resetPillPosition() { UserDefaults.standard.removeObject(forKey: Constants.UserDefaultsKeys.pillOffsetX) UserDefaults.standard.removeObject(forKey: Constants.UserDefaultsKeys.pillOffsetY) diff --git a/AgentGlance/Utilities/Constants.swift b/AgentGlance/Utilities/Constants.swift index fa06ed6..8cde047 100644 --- a/AgentGlance/Utilities/Constants.swift +++ b/AgentGlance/Utilities/Constants.swift @@ -38,6 +38,7 @@ enum Constants { static let rowDetailTemplate = "rowDetailTemplate" static let windowMode = "windowMode" static let localRemoteEnabled = "localRemoteEnabled" + static let hideOverlayWhenEmpty = "hideOverlayWhenEmpty" } } diff --git a/AgentGlance/Utilities/NotchWindow.swift b/AgentGlance/Utilities/NotchWindow.swift index 902252f..56ba88a 100644 --- a/AgentGlance/Utilities/NotchWindow.swift +++ b/AgentGlance/Utilities/NotchWindow.swift @@ -612,4 +612,8 @@ extension UserDefaults { @objc dynamic var screenSelectionMode: String { string(forKey: Constants.UserDefaultsKeys.screenSelectionMode) ?? "mainScreen" } + + @objc dynamic var hideOverlayWhenEmpty: Bool { + bool(forKey: Constants.UserDefaultsKeys.hideOverlayWhenEmpty) + } } diff --git a/AgentGlance/Views/SettingsView.swift b/AgentGlance/Views/SettingsView.swift index 92942db..3cf1cc5 100644 --- a/AgentGlance/Views/SettingsView.swift +++ b/AgentGlance/Views/SettingsView.swift @@ -124,6 +124,7 @@ private struct GeneralPane: View { @AppStorage(Constants.UserDefaultsKeys.autoExpandOnApproval) private var autoExpand = false @AppStorage(Constants.UserDefaultsKeys.suppressExpansionWhenInTerminal) private var suppressInTerminal = false @AppStorage(Constants.UserDefaultsKeys.showAllApprovals) private var showAllApprovals = false + @AppStorage(Constants.UserDefaultsKeys.hideOverlayWhenEmpty) private var hideOverlayWhenEmpty = false @AppStorage(Constants.UserDefaultsKeys.screenSelectionMode) private var screenMode = "mainScreen" @AppStorage(Constants.UserDefaultsKeys.selectedScreenID) private var selectedScreenID = "" @AppStorage(Constants.UserDefaultsKeys.keyboardNavMode) private var navMode = KeyboardNavMode.arrows.rawValue @@ -194,6 +195,7 @@ private struct GeneralPane: View { .padding(.leading, 16) } Toggle("Show all queued approvals at once", isOn: $showAllApprovals) + Toggle("Hide overlay when no sessions are active", isOn: $hideOverlayWhenEmpty) } Section("Hotkey") {