diff --git a/apps/macos/Sources/OpenClaw/Onboarding.swift b/apps/macos/Sources/OpenClaw/Onboarding.swift index 5bc7db526502..aa27eb410dae 100644 --- a/apps/macos/Sources/OpenClaw/Onboarding.swift +++ b/apps/macos/Sources/OpenClaw/Onboarding.swift @@ -605,10 +605,17 @@ final class OnboardingController: NSObject, NSWindowDelegate { func close() { self.busyReason = nil + // AppKit ignores close while its modal sheet is still attached. + self.dismissAttachedSheet() self.window?.close() self.window = nil } + func dismissAttachedSheet() { + guard let window, let sheet = window.attachedSheet else { return } + window.endSheet(sheet) + } + func setWindowCloseEnabled(_ enabled: Bool) { self.window?.standardWindowButton(.closeButton)?.isEnabled = enabled } diff --git a/apps/macos/Sources/OpenClaw/OnboardingView+Monitoring.swift b/apps/macos/Sources/OpenClaw/OnboardingView+Monitoring.swift index 8b8e5c2fed31..a75a60c97f83 100644 --- a/apps/macos/Sources/OpenClaw/OnboardingView+Monitoring.swift +++ b/apps/macos/Sources/OpenClaw/OnboardingView+Monitoring.swift @@ -94,8 +94,24 @@ extension OnboardingView { } } + static func shouldResolveInstallPromptForRunningGateway( + gatewayStatus: GatewayProcessManager.Status, + isLocal: Bool, + phase: CLIInstallPhase) -> Bool + { + guard isLocal, phase == .choosingTarget else { return false } + return switch gatewayStatus { + case .running, .attachedExisting: true + case .stopped, .starting, .failed: false + } + } + func reviseCLIActivationFailureIfGatewayReady(_ status: GatewayProcessManager.Status) { - guard Self.shouldReviseCLIActivationFailure( + let resolvesInstallPrompt = Self.shouldResolveInstallPromptForRunningGateway( + gatewayStatus: status, + isLocal: state.connectionMode == .local, + phase: cliInstallPhase) + guard resolvesInstallPrompt || Self.shouldReviseCLIActivationFailure( gatewayStatus: status, isLocal: state.connectionMode == .local, executableReady: cliExecutableReady, @@ -104,6 +120,10 @@ extension OnboardingView { cliInstalled = true cliStatusKnown = true cliStatus = nil + if resolvesInstallPrompt { + // A running local gateway already fulfills the pending install prompt. + OnboardingController.shared.dismissAttachedSheet() + } } /// LocalGatewayActivation.failed carries the reason bound to that specific activation @@ -167,6 +187,8 @@ extension OnboardingView { guard let target = await CLIInstallPrompter.shared.installTargetForCurrentBuild( presentingSheetOn: OnboardingController.shared.sheetPresentationWindow) else { + // Gateway readiness can resolve onboarding while this sheet is open. + guard !cliInstalled else { return } cliStatus = "CLI installation cancelled." return } diff --git a/apps/macos/Tests/OpenClawIPCTests/OnboardingViewSmokeTests.swift b/apps/macos/Tests/OpenClawIPCTests/OnboardingViewSmokeTests.swift index 6499f478b028..ee832ca437ee 100644 --- a/apps/macos/Tests/OpenClawIPCTests/OnboardingViewSmokeTests.swift +++ b/apps/macos/Tests/OpenClawIPCTests/OnboardingViewSmokeTests.swift @@ -255,6 +255,33 @@ struct OnboardingViewSmokeTests { installed: false)) } + @Test func `running local gateway resolves only its pending CLI install prompt`() { + for status in [GatewayProcessManager.Status.running(details: nil), .attachedExisting(details: "pid 4242")] { + #expect(OnboardingView.shouldResolveInstallPromptForRunningGateway( + gatewayStatus: status, + isLocal: true, + phase: .choosingTarget)) + } + for status in [GatewayProcessManager.Status.starting, .stopped, .failed("unavailable")] { + #expect(!OnboardingView.shouldResolveInstallPromptForRunningGateway( + gatewayStatus: status, + isLocal: true, + phase: .choosingTarget)) + } + for mode in [AppState.ConnectionMode.remote, .unconfigured] { + #expect(!OnboardingView.shouldResolveInstallPromptForRunningGateway( + gatewayStatus: .running(details: nil), + isLocal: mode == .local, + phase: .choosingTarget)) + } + for phase in [OnboardingView.CLIInstallPhase.idle, .installing, .startingService] { + #expect(!OnboardingView.shouldResolveInstallPromptForRunningGateway( + gatewayStatus: .running(details: nil), + isLocal: true, + phase: phase)) + } + } + @Test func `gateway start failure message retains the concrete reason`() { #expect( OnboardingView.gatewayStartFailureMessage(