From a85db8cea72020e168ee25a1346569c34a87f01a Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 23 Aug 2026 09:46:16 -0700 Subject: [PATCH] fix(macos): resolve the onboarding install prompt when the gateway is already running (#128273) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #128194. Local-mode onboarding has two rightful actors on the CLI page: the page's own install flow, which may open an install-target prompt on unreleased builds, and the connection-mode commit, which starts the gateway through ConnectionModeCoordinator the moment the user picks This Mac. When the gateway comes up on its own (dev-root builds, or an externally attached gateway), AI setup auto-connects and finish() runs — but close() was silently ineffective because AppKit ignores NSWindow.close() while a sheet is attached. The result, reproduced live with os_log instrumentation: a completed onboarding (dashboard open, onboardingSeen set) with a zombie onboarding window showing a dead CLI page and a stale channel-choice sheet. Two coupled fixes. OnboardingController.close() now ends any attached sheet before closing, so completion always tears the window down. And a running local gateway now resolves a pending install prompt directly: a new tested static (shouldResolveInstallPromptForRunningGateway) extends the existing gateway-status revise path to the choosingTarget phase, marking the step installed and dismissing the moot sheet; runCLIInstall returns quietly in that case instead of writing a cancellation over a resolved step. Genuine declines keep the cancelled status. --- apps/macos/Sources/OpenClaw/Onboarding.swift | 7 +++++ .../OpenClaw/OnboardingView+Monitoring.swift | 24 ++++++++++++++++- .../OnboardingViewSmokeTests.swift | 27 +++++++++++++++++++ 3 files changed, 57 insertions(+), 1 deletion(-) 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(