From ac9e3d8ae09265dfc88a5b826733dac428fd15e9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Jul 2026 15:14:12 +0000 Subject: [PATCH 1/2] Initial plan From 06423de3213efb7643abf9fed91a707e4d22c4d0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Jul 2026 15:26:10 +0000 Subject: [PATCH 2/2] Only open browser on initial webView scene connection (#4985) --- HomeAssistant.xcodeproj/project.pbxproj | 8 +++ .../App/Scenes/MacBrowserSceneLauncher.swift | 31 +++++++++++ .../QuickActionWindowSceneDelegate.swift | 11 +++- .../Scenes/MacBrowserSceneLauncher.test.swift | 52 +++++++++++++++++++ 4 files changed, 100 insertions(+), 2 deletions(-) create mode 100644 Sources/App/Scenes/MacBrowserSceneLauncher.swift create mode 100644 Tests/App/Scenes/MacBrowserSceneLauncher.test.swift diff --git a/HomeAssistant.xcodeproj/project.pbxproj b/HomeAssistant.xcodeproj/project.pbxproj index b38eec9b3b..28f90a769f 100644 --- a/HomeAssistant.xcodeproj/project.pbxproj +++ b/HomeAssistant.xcodeproj/project.pbxproj @@ -720,6 +720,7 @@ 425573D12B5576E600145217 /* CarPlayDomainsListTemplate+Build.swift in Sources */ = {isa = PBXBuildFile; fileRef = 425573D02B5576E600145217 /* CarPlayDomainsListTemplate+Build.swift */; }; 425573D32B55770D00145217 /* CarPlayDomainsListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 425573D22B55770D00145217 /* CarPlayDomainsListViewModel.swift */; }; 425573DA2B57DDE000145217 /* WindowScenesManager.test.swift in Sources */ = {isa = PBXBuildFile; fileRef = 425573D92B57DDE000145217 /* WindowScenesManager.test.swift */; }; + 52386BFCFAFCBE783D0BCDD9 /* MacBrowserSceneLauncher.test.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26BE2F311BDB2F205F7432FF /* MacBrowserSceneLauncher.test.swift */; }; 425573EB2B588FFB00145217 /* CarPlayListItemProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 425573EA2B588FFB00145217 /* CarPlayListItemProvider.swift */; }; 425573ED2B58904000145217 /* CarPlayEntityListItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 425573EC2B58904000145217 /* CarPlayEntityListItem.swift */; }; 4256A70D2F5ECBB3003D464F /* SpeechTranscriber.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4256A70C2F5ECBB3003D464F /* SpeechTranscriber.swift */; }; @@ -1688,6 +1689,7 @@ DA6F4C18D66EDBA5DCEAE833 /* DatabaseMigration.test.swift in Sources */ = {isa = PBXBuildFile; fileRef = 892F0EF22A0B9F20AAEE4CCA /* DatabaseMigration.test.swift */; }; DB54626ADCE0C32094C8C0B9 /* LoadingButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BFD0D30836C69840AB63A8A /* LoadingButton.swift */; }; DE8048842D9FB6C90459E229 /* QuickActionWindowSceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1948705C52B43ACD36FD0B32 /* QuickActionWindowSceneDelegate.swift */; }; + 1A693E34211ADFAB76155607 /* MacBrowserSceneLauncher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 730D7B17F8A2B19BCA181F91 /* MacBrowserSceneLauncher.swift */; }; DFB59CC17BC47B8A6949B81C /* KioskScreensaverView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71373DD7455D9CE893EB450B /* KioskScreensaverView.swift */; }; E33F645682064B22AC4CDFBB /* SFSafeSymbols in Frameworks */ = {isa = PBXBuildFile; productRef = C6ACD1840F0E4F5FBFDDE88B /* SFSafeSymbols */; }; E3A02409794174F002C8BB4F /* IconSearchPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BC23DE131CA8813C2DBD657 /* IconSearchPicker.swift */; }; @@ -2344,6 +2346,7 @@ 1861EB0361816DC9260D1F5E /* ComplicationFamilySelectView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ComplicationFamilySelectView.swift; sourceTree = ""; }; 1945E8E386664E5A88668E77 /* WebViewController+ReAuth.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WebViewController+ReAuth.swift"; sourceTree = ""; }; 1948705C52B43ACD36FD0B32 /* QuickActionWindowSceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickActionWindowSceneDelegate.swift; sourceTree = ""; }; + 730D7B17F8A2B19BCA181F91 /* MacBrowserSceneLauncher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacBrowserSceneLauncher.swift; sourceTree = ""; }; 1CCF41D72CA895DB501333EF /* KioskModeManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KioskModeManager.swift; sourceTree = ""; }; 207E35C8F1554A9AD616FFA2 /* Pods-iOS-Extensions-Share-metadata.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "Pods-iOS-Extensions-Share-metadata.plist"; path = "Pods/Pods-iOS-Extensions-Share-metadata.plist"; sourceTree = ""; }; 208A5362BE60377368ACB1D6 /* RealmResultsObserver.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RealmResultsObserver.swift; sourceTree = ""; }; @@ -2582,6 +2585,7 @@ 425573D02B5576E600145217 /* CarPlayDomainsListTemplate+Build.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CarPlayDomainsListTemplate+Build.swift"; sourceTree = ""; }; 425573D22B55770D00145217 /* CarPlayDomainsListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarPlayDomainsListViewModel.swift; sourceTree = ""; }; 425573D92B57DDE000145217 /* WindowScenesManager.test.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowScenesManager.test.swift; sourceTree = ""; }; + 26BE2F311BDB2F205F7432FF /* MacBrowserSceneLauncher.test.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacBrowserSceneLauncher.test.swift; sourceTree = ""; }; 425573E42B58380D00145217 /* CarPlay.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CarPlay.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX14.0.sdk/System/iOSSupport/System/Library/Frameworks/CarPlay.framework; sourceTree = DEVELOPER_DIR; }; 425573EA2B588FFB00145217 /* CarPlayListItemProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarPlayListItemProvider.swift; sourceTree = ""; }; 425573EC2B58904000145217 /* CarPlayEntityListItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarPlayEntityListItem.swift; sourceTree = ""; }; @@ -4521,6 +4525,7 @@ 118261F424F8C7C1000795C6 /* SceneManager.swift */, FD3BC66229B9FF8F00B19FBE /* CarPlaySceneDelegate.swift */, 1948705C52B43ACD36FD0B32 /* QuickActionWindowSceneDelegate.swift */, + 730D7B17F8A2B19BCA181F91 /* MacBrowserSceneLauncher.swift */, 42F1DA5A2B4BF7DF002729BC /* WindowSizeObserver.swift */, 42F1DA5C2B4BF85F002729BC /* WindowScenesManager.swift */, ); @@ -4683,6 +4688,7 @@ isa = PBXGroup; children = ( 425573D92B57DDE000145217 /* WindowScenesManager.test.swift */, + 26BE2F311BDB2F205F7432FF /* MacBrowserSceneLauncher.test.swift */, ); path = Scenes; sourceTree = ""; @@ -9673,6 +9679,7 @@ 11195F6F267EFC8E003DF674 /* NotificationManagerLocalPushInterfaceDirect.swift in Sources */, FD3BC66329B9FF8F00B19FBE /* CarPlaySceneDelegate.swift in Sources */, DE8048842D9FB6C90459E229 /* QuickActionWindowSceneDelegate.swift in Sources */, + 1A693E34211ADFAB76155607 /* MacBrowserSceneLauncher.swift in Sources */, 11C4629424B189B100031902 /* NotificationRateLimitsAPI.swift in Sources */, 11A71C6B24A463FC00D9565F /* ZoneManagerState.swift in Sources */, 42106F562F447C6B009351FC /* MTLSLabsLabel.swift in Sources */, @@ -9981,6 +9988,7 @@ 42ACC2AF2E9FA1D60045A3FD /* DiscoveredHomeAssistant.test.swift in Sources */, 117D8A0A24A9381F00580913 /* UIColor+CSSRGB.test.swift in Sources */, 425573DA2B57DDE000145217 /* WindowScenesManager.test.swift in Sources */, + 52386BFCFAFCBE783D0BCDD9 /* MacBrowserSceneLauncher.test.swift in Sources */, 11ED43A027279AFA00B5FD45 /* OnboardingAuthLoginImpl.test.swift in Sources */, 11EFD3BE27253504000AF78B /* OnboardingAuthStepConnectivity.test.swift in Sources */, 42E3B8C02D8ACE0400F5D084 /* OptionalsDefaultTests.swift in Sources */, diff --git a/Sources/App/Scenes/MacBrowserSceneLauncher.swift b/Sources/App/Scenes/MacBrowserSceneLauncher.swift new file mode 100644 index 0000000000..69ce5eaa50 --- /dev/null +++ b/Sources/App/Scenes/MacBrowserSceneLauncher.swift @@ -0,0 +1,31 @@ +import Shared + +/// Gates the Mac "Open Home Assistant UI in browser" (`macNativeFeaturesOnly`) auto-launch that runs when a +/// `"WebView"` scene connects (see `QuickActionWindowSceneDelegate`). +/// +/// Only the *first* scene connection of the process — the user's cold launch — should open Home Assistant in +/// the browser and destroy the otherwise-empty window. macOS reconnects this scene in the background throughout +/// the app's lifetime (for state restoration and other background work), and every reconnection used to run the +/// browser auto-launch, so the dashboard reopened every few minutes even though the user never asked for it +/// (#4985). Later, genuinely user-initiated opens while the app is already running are handled elsewhere — the +/// Dock reopen handler and the status-item menu both go through `StatusItemPrimaryAction.openInBrowserIfNeeded()` +/// — so this scene-connection path only ever needs to cover the cold launch. +enum MacBrowserSceneLauncher { + /// Whether the initial (cold-launch) `"WebView"` scene connection has already been observed this process. + /// Reset only implicitly, by the process being relaunched. + static var didHandleInitialSceneConnection = false + + /// Records that a `"WebView"` scene has connected and reports whether this was the first connection of the + /// process (i.e. the user's cold launch). Subsequent calls return `false`. + @discardableResult + static func markSceneConnected() -> Bool { + let isInitialConnection = !didHandleInitialSceneConnection + didHandleInitialSceneConnection = true + return isInitialConnection + } + + /// Whether the "Open Home Assistant UI in browser" behaviour applies on this platform/configuration. + static var isBrowserLaunchEnabled: Bool { + Current.isCatalyst && Current.settingsStore.macNativeFeaturesOnly + } +} diff --git a/Sources/App/Scenes/QuickActionWindowSceneDelegate.swift b/Sources/App/Scenes/QuickActionWindowSceneDelegate.swift index d0cb7b05c4..975ba570f3 100644 --- a/Sources/App/Scenes/QuickActionWindowSceneDelegate.swift +++ b/Sources/App/Scenes/QuickActionWindowSceneDelegate.swift @@ -27,6 +27,11 @@ final class QuickActionWindowSceneDelegate: UIResponder, UIWindowSceneDelegate { ) { guard let windowScene = scene as? UIWindowScene else { return } + // Record this connection and remember whether it was the process's first (cold-launch) one. macOS + // reconnects this scene in the background throughout the app's lifetime, so only the first connection + // represents a user-initiated launch (see `MacBrowserSceneLauncher`). + let isInitialSceneConnection = MacBrowserSceneLauncher.markSceneConnected() + // Cold launch via a quick action: the app was launched by tapping an app-icon shortcut. // This getter does not exist on macOS 10.15, so check that it responds before accessing it. if connectionOptions.responds(to: #selector(getter: UIScene.ConnectionOptions.shortcutItem)), @@ -38,8 +43,10 @@ final class QuickActionWindowSceneDelegate: UIResponder, UIWindowSceneDelegate { // "Open Home Assistant UI in browser" (Mac): when the app is opened by tapping its icon, open // Home Assistant in the default browser and destroy the empty webview window so none is left behind. // This only runs on macOS, where the last-known network information is read live from macBridge, - // so the synchronous evaluation is current. - if Current.isCatalyst, Current.settingsStore.macNativeFeaturesOnly, + // so the synchronous evaluation is current. Only the initial (cold-launch) connection triggers this; + // later background scene reconnections must not reopen the browser (#4985). Subsequent user-initiated + // opens are handled by the Dock reopen handler / status-item menu via `StatusItemPrimaryAction`. + if isInitialSceneConnection, MacBrowserSceneLauncher.isBrowserLaunchEnabled, let url = Current.servers.all.first?.activeURLUsingLastKnownNetworkState() { URLOpener.shared.open(url, options: [:], completionHandler: nil) UIApplication.shared.requestSceneSessionDestruction(session, options: nil, errorHandler: nil) diff --git a/Tests/App/Scenes/MacBrowserSceneLauncher.test.swift b/Tests/App/Scenes/MacBrowserSceneLauncher.test.swift new file mode 100644 index 0000000000..3655189ca2 --- /dev/null +++ b/Tests/App/Scenes/MacBrowserSceneLauncher.test.swift @@ -0,0 +1,52 @@ +@testable import HomeAssistant +@testable import Shared +import XCTest + +final class MacBrowserSceneLauncherTests: XCTestCase { + private var originalIsCatalyst: Bool! + private var originalMacNativeFeaturesOnly: Bool! + + override func setUpWithError() throws { + try super.setUpWithError() + originalIsCatalyst = Current.isCatalyst + originalMacNativeFeaturesOnly = Current.settingsStore.macNativeFeaturesOnly + MacBrowserSceneLauncher.didHandleInitialSceneConnection = false + } + + override func tearDownWithError() throws { + Current.isCatalyst = originalIsCatalyst + Current.settingsStore.macNativeFeaturesOnly = originalMacNativeFeaturesOnly + MacBrowserSceneLauncher.didHandleInitialSceneConnection = false + try super.tearDownWithError() + } + + func testMarkSceneConnectedReturnsTrueOnlyForInitialConnection() { + XCTAssertTrue(MacBrowserSceneLauncher.markSceneConnected()) + XCTAssertFalse(MacBrowserSceneLauncher.markSceneConnected()) + XCTAssertFalse(MacBrowserSceneLauncher.markSceneConnected()) + } + + func testMarkSceneConnectedSetsHandledFlag() { + XCTAssertFalse(MacBrowserSceneLauncher.didHandleInitialSceneConnection) + MacBrowserSceneLauncher.markSceneConnected() + XCTAssertTrue(MacBrowserSceneLauncher.didHandleInitialSceneConnection) + } + + func testBrowserLaunchEnabledRequiresCatalystAndPreference() { + Current.isCatalyst = true + Current.settingsStore.macNativeFeaturesOnly = true + XCTAssertTrue(MacBrowserSceneLauncher.isBrowserLaunchEnabled) + } + + func testBrowserLaunchDisabledWhenPreferenceOff() { + Current.isCatalyst = true + Current.settingsStore.macNativeFeaturesOnly = false + XCTAssertFalse(MacBrowserSceneLauncher.isBrowserLaunchEnabled) + } + + func testBrowserLaunchDisabledWhenNotCatalyst() { + Current.isCatalyst = false + Current.settingsStore.macNativeFeaturesOnly = true + XCTAssertFalse(MacBrowserSceneLauncher.isBrowserLaunchEnabled) + } +}