Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 43 additions & 7 deletions AgentGlance/App/AppState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -123,6 +124,7 @@ final class AppState {
registerGlobalHotkey()
observeAppearanceChanges()
observeScreenModeChanges()
observeHideOverlayChanges()
sessionManager.startLivenessChecks()

// Start web remote server if enabled
Expand All @@ -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)
Expand Down Expand Up @@ -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() {
Expand All @@ -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
Expand All @@ -383,9 +411,7 @@ final class AppState {
func refreshNotchWindow() {
notchWindow?.close()
notchWindow = nil
if !sessionManager.activeSessions.isEmpty {
createNotchWindow()
}
createNotchWindow()
}

// MARK: - Testing
Expand Down Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions AgentGlance/Utilities/Constants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ enum Constants {
static let rowDetailTemplate = "rowDetailTemplate"
static let windowMode = "windowMode"
static let localRemoteEnabled = "localRemoteEnabled"
static let hideOverlayWhenEmpty = "hideOverlayWhenEmpty"
}
}

Expand Down
4 changes: 4 additions & 0 deletions AgentGlance/Utilities/NotchWindow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
2 changes: 2 additions & 0 deletions AgentGlance/Views/SettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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") {
Expand Down