mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 05:16:23 -06:00
fix(macos): attach the onboarding CLI install prompt to the window (#128183)
* fix(macos): attach the onboarding CLI install prompt to the window On an unreleased build, the onboarding CLI page resolved its install target through NSAlert.runModal() — a detached app-modal panel that is absent from the app's AX window list and freely covered by system permission dialogs. Live repro: with two TCC prompts stacked over it, the main thread sat parked in runModal for 30+ minutes while the page showed an active "Install OpenClaw" spinner, the close button was disabled, and busyReason claimed an install was in flight — before the user had chosen anything. A spinner that means "answer a dialog you cannot see" is a silent-failure trap. The prompt (both the confirm-stable alert and the channel chooser) is now a sheet attached to the onboarding window via beginSheetModal, so it stays z-ordered with the window and AX-visible. The busy state is honest: a new .choosingTarget phase renders the install row as pending instead of running, and installingCLI/close-disabled/busyReason only engage after a target is actually chosen. Non-onboarding callers (checkAndPromptIfNeeded) keep runModal by passing no window. All alert text and button order unchanged. * style(macos): wrap sheetPresentationWindow property body
This commit is contained in:
committed by
GitHub
parent
cbc89fc5a1
commit
47436e447e
@@ -60,14 +60,17 @@ final class CLIInstallPrompter {
|
||||
guard lastPrompt != version else { return }
|
||||
AppDefaults.standard.set(version, forKey: cliInstallPromptedVersionKey)
|
||||
|
||||
if let target = self.installTargetForCurrentBuild(confirmStable: true) {
|
||||
if let target = await self.installTargetForCurrentBuild(confirmStable: true, presentingSheetOn: nil) {
|
||||
Task { _ = await self.installCLI(target: target) }
|
||||
}
|
||||
|
||||
self.logger.debug("cli install prompt handled reason=\(reason, privacy: .public)")
|
||||
}
|
||||
|
||||
func installTargetForCurrentBuild(confirmStable: Bool = false) -> CLIInstaller.InstallTarget? {
|
||||
func installTargetForCurrentBuild(
|
||||
confirmStable: Bool = false,
|
||||
presentingSheetOn window: NSWindow?) async -> CLIInstaller.InstallTarget?
|
||||
{
|
||||
let appVersion = Self.appVersion()
|
||||
if let target = CLIInstaller.automaticInstallTarget(
|
||||
appVersion: appVersion,
|
||||
@@ -80,7 +83,7 @@ final class CLIInstallPrompter {
|
||||
alert.addButton(withTitle: "Install CLI")
|
||||
alert.addButton(withTitle: "Not Now")
|
||||
alert.addButton(withTitle: "Open Settings")
|
||||
switch alert.runModal() {
|
||||
switch await self.present(alert, presentingSheetOn: window) {
|
||||
case .alertFirstButtonReturn:
|
||||
return target
|
||||
case .alertThirdButtonReturn:
|
||||
@@ -91,14 +94,18 @@ final class CLIInstallPrompter {
|
||||
}
|
||||
}
|
||||
|
||||
return self.chooseChannel(
|
||||
return await self.chooseChannel(
|
||||
suggested: CLIInstaller.suggestedChannel(
|
||||
appVersion: appVersion,
|
||||
isDebug: CLIInstallBuild.isDebug))
|
||||
isDebug: CLIInstallBuild.isDebug),
|
||||
presentingSheetOn: window)
|
||||
.map(CLIInstaller.InstallTarget.channel)
|
||||
}
|
||||
|
||||
private func chooseChannel(suggested: CLIInstaller.Channel) -> CLIInstaller.Channel? {
|
||||
private func chooseChannel(
|
||||
suggested: CLIInstaller.Channel,
|
||||
presentingSheetOn window: NSWindow?) async -> CLIInstaller.Channel?
|
||||
{
|
||||
let channels = [suggested] + CLIInstaller.Channel.allCases.filter { $0 != suggested }
|
||||
let alert = NSAlert()
|
||||
alert.messageText = "Choose OpenClaw CLI channel"
|
||||
@@ -111,12 +118,18 @@ final class CLIInstallPrompter {
|
||||
alert.addButton(withTitle: channel.label)
|
||||
}
|
||||
alert.addButton(withTitle: "Not Now")
|
||||
let response = alert.runModal()
|
||||
let response = await self.present(alert, presentingSheetOn: window)
|
||||
let index = response.rawValue - NSApplication.ModalResponse.alertFirstButtonReturn.rawValue
|
||||
guard channels.indices.contains(index) else { return nil }
|
||||
return channels[index]
|
||||
}
|
||||
|
||||
private func present(_ alert: NSAlert, presentingSheetOn window: NSWindow?) async -> NSApplication.ModalResponse {
|
||||
// Attaching onboarding alerts preserves their AX visibility and window-relative z-order.
|
||||
guard let window else { return alert.runModal() }
|
||||
return await alert.beginSheetModal(for: window)
|
||||
}
|
||||
|
||||
private func installCLI(
|
||||
target: CLIInstaller.InstallTarget,
|
||||
showCompletionAlert: Bool = true,
|
||||
|
||||
@@ -539,6 +539,10 @@ final class OnboardingController: NSObject, NSWindowDelegate {
|
||||
static let shared = OnboardingController()
|
||||
static let windowStyleMask: NSWindow.StyleMask = [.titled, .closable, .resizable, .fullSizeContentView]
|
||||
private var window: NSWindow?
|
||||
var sheetPresentationWindow: NSWindow? {
|
||||
self.window
|
||||
}
|
||||
|
||||
/// Human description of work in flight ("Installing the Gateway…").
|
||||
/// While set, closing the window asks for confirmation instead of quitting
|
||||
/// setup mid-operation.
|
||||
@@ -638,6 +642,7 @@ final class OnboardingController: NSObject, NSWindowDelegate {
|
||||
struct OnboardingView: View {
|
||||
enum CLIInstallPhase {
|
||||
case idle
|
||||
case choosingTarget
|
||||
case installing
|
||||
case startingService
|
||||
}
|
||||
|
||||
@@ -146,9 +146,6 @@ extension OnboardingView {
|
||||
func startCLIInstall() {
|
||||
guard self.onboardingVisible, !installingCLI else { return }
|
||||
installingCLI = true
|
||||
OnboardingController.shared.setWindowCloseEnabled(false)
|
||||
// Cmd-W bypasses the disabled close button; the delegate asks first.
|
||||
OnboardingController.shared.busyReason = "OpenClaw is installing the Gateway service."
|
||||
Task { @MainActor in await self.runCLIInstall() }
|
||||
}
|
||||
|
||||
@@ -159,17 +156,24 @@ extension OnboardingView {
|
||||
}
|
||||
|
||||
func runCLIInstall() async {
|
||||
self.cliInstallPhase = .installing
|
||||
self.cliInstallPhase = .choosingTarget
|
||||
defer {
|
||||
self.installingCLI = false
|
||||
self.cliInstallPhase = .idle
|
||||
OnboardingController.shared.setWindowCloseEnabled(true)
|
||||
OnboardingController.shared.busyReason = nil
|
||||
}
|
||||
guard let target = CLIInstallPrompter.shared.installTargetForCurrentBuild() else {
|
||||
// Choosing a target is not installation: keep its spinner and close/busy guards inactive.
|
||||
guard let target = await CLIInstallPrompter.shared.installTargetForCurrentBuild(
|
||||
presentingSheetOn: OnboardingController.shared.sheetPresentationWindow)
|
||||
else {
|
||||
cliStatus = "CLI installation cancelled."
|
||||
return
|
||||
}
|
||||
self.cliInstallPhase = .installing
|
||||
OnboardingController.shared.setWindowCloseEnabled(false)
|
||||
// Cmd-W bypasses the disabled close button; the delegate asks first.
|
||||
OnboardingController.shared.busyReason = "OpenClaw is installing the Gateway service."
|
||||
let installed = await CLIInstaller.install(target: target) { message in
|
||||
self.cliStatus = message
|
||||
}
|
||||
|
||||
@@ -879,6 +879,7 @@ extension OnboardingView {
|
||||
private var installStepStateForInstall: InstallStepState {
|
||||
if self.cliInstalled { return .done }
|
||||
if self.installingCLI {
|
||||
if self.cliInstallPhase == .choosingTarget { return .pending }
|
||||
return self.cliInstallPhase == .startingService ? .done : .running
|
||||
}
|
||||
if self.installFailed { return .failed }
|
||||
|
||||
Reference in New Issue
Block a user