From 17643673c33decfd3ab9ccd72bcdd57308b3ce57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Pantale=C3=A3o?= <5808343+bgoncal@users.noreply.github.com> Date: Fri, 19 Jun 2026 17:18:23 +0200 Subject: [PATCH 1/4] Fix macOS menu-bar icon not activating the main window The primary webView WindowGroup was the only scene missing handlesExternalEvents(matching:), so a scene activation request carrying targetContentIdentifier "ha.webview" (issued when tapping the menu-bar item with no open window) had no WindowGroup to bind to and no window appeared. Match the identifier here, consistent with the other groups. Co-Authored-By: Claude Opus 4.8 --- Sources/App/HAApp.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Sources/App/HAApp.swift b/Sources/App/HAApp.swift index a1f1ae745..8a9553aa1 100644 --- a/Sources/App/HAApp.swift +++ b/Sources/App/HAApp.swift @@ -14,6 +14,11 @@ struct HAApp: App { .onOpenURL { handleIncoming(url: $0) } .onContinueUserActivity(NSUserActivityTypeBrowsingWeb) { handleIncoming(userActivity: $0) } } + // Without this, `activateAnyScene(for: .webView)` (e.g. tapping the macOS menu-bar item) requests a + // new scene carrying `targetContentIdentifier == "ha.webview"`; with no `WindowGroup` advertising that + // identifier SwiftUI can't bind the scene to a group and no window appears. Matching it here — like the + // other groups below — routes the activation to this group so the window becomes visible. + .handlesExternalEvents(matching: [SceneActivity.webView.activityIdentifier]) // Mac Settings WindowGroup { From df1cb7993fe4f691e480218df935e90958d9dda5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Pantale=C3=A3o?= <5808343+bgoncal@users.noreply.github.com> Date: Fri, 19 Jun 2026 17:56:30 +0200 Subject: [PATCH 2/4] Offer Open App / Open Settings on macOS menu-bar right-click MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The status-item right-click menu's first action was an ambiguous "Toggle Home Assistant". Replace it with two explicit actions — "Open Home Assistant" (activates the main window) and "Open Settings…" (activates the Settings window, keeping the ⌘, shortcut) — so a right-click clearly offers opening the app or its settings. The left-click show/hide toggle behaviour is unchanged. Co-Authored-By: Claude Opus 4.8 --- .../Resources/en.lproj/Localizable.strings | 2 + Sources/App/Utilities/MenuManager.swift | 38 +++++++++---------- .../Shared/Resources/Swiftgen/Strings.swift | 6 +++ 3 files changed, 25 insertions(+), 21 deletions(-) diff --git a/Sources/App/Resources/en.lproj/Localizable.strings b/Sources/App/Resources/en.lproj/Localizable.strings index 20d255a0c..ca18c2447 100644 --- a/Sources/App/Resources/en.lproj/Localizable.strings +++ b/Sources/App/Resources/en.lproj/Localizable.strings @@ -659,6 +659,8 @@ This server requires a client certificate (mTLS) but the operation was cancelled "menu.application.preferences" = "Preferences…"; "menu.file.update_sensors" = "Update Sensors"; "menu.help.help" = "%@ Help"; +"menu.status_item.open" = "Open %1$@"; +"menu.status_item.open_settings" = "Open Settings…"; "menu.status_item.quit" = "Quit"; "menu.status_item.toggle" = "Toggle %1$@"; "menu.view.find" = "Find"; diff --git a/Sources/App/Utilities/MenuManager.swift b/Sources/App/Utilities/MenuManager.swift index e63b9f603..3c54f6a09 100644 --- a/Sources/App/Utilities/MenuManager.swift +++ b/Sources/App/Utilities/MenuManager.swift @@ -286,17 +286,6 @@ class MenuManager { ) } - private func preferencesMenu() -> AppMacBridgeStatusItemMenuItem { - .init( - name: L10n.Menu.Application.preferences, - keyEquivalentModifier: [.command], - keyEquivalent: "," - ) { callbackInfo in - Current.sceneManager.activateAnyScene(for: .settings) - callbackInfo.activate() - } - } - private func helpMenus() -> [UIMenu] { let title = L10n.Menu.Help.help(appName) @@ -367,14 +356,21 @@ class MenuManager { ) } - private func toggleMenu() -> AppMacBridgeStatusItemMenuItem { - .init(name: L10n.Menu.StatusItem.toggle(appName)) { callbackInfo in - if callbackInfo.isActive { - callbackInfo.deactivate() - } else { - Current.sceneManager.activateAnyScene(for: .webView) - callbackInfo.activate() - } + private func openAppMenu() -> AppMacBridgeStatusItemMenuItem { + .init(name: L10n.Menu.StatusItem.open(appName)) { callbackInfo in + Current.sceneManager.activateAnyScene(for: .webView) + callbackInfo.activate() + } + } + + private func openSettingsMenu() -> AppMacBridgeStatusItemMenuItem { + .init( + name: L10n.Menu.StatusItem.openSettings, + keyEquivalentModifier: [.command], + keyEquivalent: "," + ) { callbackInfo in + Current.sceneManager.activateAnyScene(for: .settings) + callbackInfo.activate() } } @@ -397,10 +393,10 @@ class MenuManager { } var menuItems = [AppMacBridgeStatusItemMenuItem]() - menuItems.append(toggleMenu()) + menuItems.append(openAppMenu()) + menuItems.append(openSettingsMenu()) menuItems.append(.separator()) menuItems.append(contentsOf: aboutMenu()) - menuItems.append(preferencesMenu()) menuItems.append(quitMenu()) Current.macBridge.configureStatusItem(using: AppMacBridgeStatusItemConfiguration( diff --git a/Sources/Shared/Resources/Swiftgen/Strings.swift b/Sources/Shared/Resources/Swiftgen/Strings.swift index 49266c5e1..5bb4b47ac 100644 --- a/Sources/Shared/Resources/Swiftgen/Strings.swift +++ b/Sources/Shared/Resources/Swiftgen/Strings.swift @@ -2412,6 +2412,12 @@ public enum L10n { } } public enum StatusItem { + /// Open %1$@ + public static func open(_ p1: Any) -> String { + return L10n.tr("Localizable", "menu.status_item.open", String(describing: p1)) + } + /// Open Settings… + public static var openSettings: String { return L10n.tr("Localizable", "menu.status_item.open_settings") } /// Quit public static var quit: String { return L10n.tr("Localizable", "menu.status_item.quit") } /// Toggle %1$@ From 882a3e15dbc6b5ca654d2ee85048ce48baab3a68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Pantale=C3=A3o?= <5808343+bgoncal@users.noreply.github.com> Date: Fri, 19 Jun 2026 18:12:46 +0200 Subject: [PATCH 3/4] Show the main window reliably from the macOS menu-bar icon MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The real reason tapping the menu-bar icon did nothing: the status-item primary action toggled on `NSApp.isActive`, but in menu-bar (.accessory) mode the app can be "active" with no visible window, so the click took the hide branch and no window ever appeared — the icon looked inert. - `isActive` now also requires a visible standard window, so a click shows the app whenever no window is on screen. - `activate()` brings an existing (hidden/ordered-out) window back to the front via makeKeyAndOrderFront, which `NSApp.activate` alone doesn't do reliably in accessory mode. New windows are still created by the scene activation request, which the WindowGroup matcher (this branch) routes. Co-Authored-By: Claude Opus 4.8 --- Sources/MacBridge/MacBridgeStatusItem.swift | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/Sources/MacBridge/MacBridgeStatusItem.swift b/Sources/MacBridge/MacBridgeStatusItem.swift index 08f9b0afb..6f74cb1e5 100644 --- a/Sources/MacBridge/MacBridgeStatusItem.swift +++ b/Sources/MacBridge/MacBridgeStatusItem.swift @@ -111,11 +111,21 @@ class MacBridgeStatusItemCallbackInfoImpl: MacBridgeStatusItemCallbackInfo { } var isActive: Bool { - NSApp.isActive + // Only treat the app as "active" (and therefore something to hide on the next click) when it + // actually has a visible standard window. In menu-bar (`.accessory`) mode `NSApp.isActive` can be + // true with no visible window — which previously made the status-item click hide the app instead of + // showing it, so the icon appeared to do nothing. + NSApp.isActive && NSApp.windows.contains { $0.isVisible && !$0.isMiniaturized && $0.level == .normal } } func activate() { NSApp.activate(ignoringOtherApps: true) + // `NSApp.activate` un-hides the app but doesn't reliably bring a closed/ordered-out window back in + // accessory mode, so surface an existing standard window here. A brand-new window (when none exists) + // is created by the scene-activation request in `SceneManager.activateAnyScene`. + NSApp.windows + .first { $0.level == .normal && !$0.isMiniaturized }? + .makeKeyAndOrderFront(nil) } func deactivate() { From 72f4ce89a0bac6253ddd01ec389f56fe8b65a770 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Pantale=C3=A3o?= <5808343+bgoncal@users.noreply.github.com> Date: Fri, 19 Jun 2026 19:11:50 +0200 Subject: [PATCH 4/4] Create the macOS status item lazily on the main thread MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause of the dead menu-bar icon (both left- and right-click did nothing even though the icon rendered and the custom menus worked): NSStatusBar/NSStatusItem are main-thread-only, but the item was built in MacBridgeStatusItem.init, which runs inside AppEnvironment's lazy initialiser — created on the first touch of `Current` anywhere. Under the SwiftUI app lifecycle that first touch can be a background thread (e.g. an early Current.Log call), so the button's target/action were wired off-main and never hooked into the main-thread event machinery: the image (set later from buildMenu, on main) still drew, but clicks were silently dropped. Defer creation+wiring to ensureStatusItem(), first reached via configure(using:) on the main-thread buildMenu path — no synchronous main-thread hop during the Current initialiser, so no deadlock risk. Also fall back to the button window's currentEvent in statusItemTapped so a configured item never drops a click if NSApp.currentEvent is nil. Co-Authored-By: Claude Opus 4.8 --- Sources/MacBridge/MacBridgeStatusItem.swift | 40 +++++++++++++++------ 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/Sources/MacBridge/MacBridgeStatusItem.swift b/Sources/MacBridge/MacBridgeStatusItem.swift index 6f74cb1e5..8345440c0 100644 --- a/Sources/MacBridge/MacBridgeStatusItem.swift +++ b/Sources/MacBridge/MacBridgeStatusItem.swift @@ -1,24 +1,39 @@ import AppKit class MacBridgeStatusItem: NSObject, NSMenuDelegate { - private let statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) + private var statusItem: NSStatusItem? private var lastConfiguration: MacBridgeStatusItemConfiguration? - override init() { - super.init() - statusItem.button?.imagePosition = .imageLeading - statusItem.button?.target = self - statusItem.button?.action = #selector(statusItemTapped(_:)) - statusItem.button?.sendAction(on: [.leftMouseUp, .leftMouseDown, .rightMouseDown]) + /// Returns the status item, creating and wiring it on first use. + /// + /// `NSStatusBar`/`NSStatusItem` are main-thread-only. This bridge is owned by `Current`, whose backing + /// `AppEnvironment` is created lazily on the first touch of `Current` anywhere — which, under the SwiftUI + /// app lifecycle, can be a background thread (e.g. an early `Current.Log` call). Creating the item and + /// wiring its button's `target`/`action` off the main thread leaves the button rendering its image (set + /// from `buildMenu`, which runs on main) yet never receiving clicks, so the icon looks inert. Deferring + /// creation to here — first reached via `configure(using:)` on the main-thread `buildMenu` path — keeps + /// all of it on the main thread without the deadlock risk of a synchronous main-thread hop during the + /// `Current` initialiser. + @discardableResult + private func ensureStatusItem() -> NSStatusItem { + if let statusItem { return statusItem } + let item = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) + item.button?.imagePosition = .imageLeading + item.button?.target = self + item.button?.action = #selector(statusItemTapped(_:)) + item.button?.sendAction(on: [.leftMouseUp, .leftMouseDown, .rightMouseDown]) + statusItem = item + return item } func configure(title: String) { - statusItem.button?.title = title + ensureStatusItem().button?.title = title } func configure(using configuration: MacBridgeStatusItemConfiguration) { lastConfiguration = configuration + let statusItem = ensureStatusItem() statusItem.isVisible = configuration.isVisible statusItem.button?.setAccessibilityLabel(configuration.accessibilityLabel) @@ -28,12 +43,15 @@ class MacBridgeStatusItem: NSObject, NSMenuDelegate { } @objc private func statusItemTapped(_ sender: NSStatusBarButton) { - guard let configuration = lastConfiguration, let event = NSApp.currentEvent else { return } + // `NSApp.currentEvent` is the click event during this action, but fall back to the button window's + // current event so a configured item never silently drops a click if it's momentarily nil. + guard let configuration = lastConfiguration, + let event = NSApp.currentEvent ?? sender.window?.currentEvent else { return } if event.isRightClickEquivalentEvent { let mainMenu = menu(for: configuration.items) mainMenu.delegate = self - statusItem.menu = mainMenu + ensureStatusItem().menu = mainMenu sender.performClick(sender) } else if event.type == .leftMouseUp { // leftMouseDown also fires, but we only want to do that for ctrl-clicks @@ -42,7 +60,7 @@ class MacBridgeStatusItem: NSObject, NSMenuDelegate { } func menuDidClose(_ menu: NSMenu) { - statusItem.menu = nil + statusItem?.menu = nil } private func modifierKeys(for uglyMask: Int) -> NSEvent.ModifierFlags {