From bed6b905053df53b765c02194f9247fb39648bce Mon Sep 17 00:00:00 2001 From: m-szymanska Date: Sun, 5 Jul 2026 23:23:17 -0700 Subject: [PATCH] fix(onboarding): open Settings via SwiftUI openSettings action (PR #53 smoke) The onboarding readiness step's 'Set up MCP servers' button routed through OnboardingViewModel.openMcpSettings(), which called NSApp.sendAction(showSettingsWindow:). In this accessory / LSUIElement app that private selector has no responder, so the click did nothing. Switch to the canonical path used by TrayMenuView / AgentChatView: the view holds @Environment(\.openSettings) and calls it after the VM arms the one-shot deep-link (SettingsDeepLink.pendingSection = .engine). The VM method is renamed openMcpSettings() -> prepareMcpSettingsDeepLink() and no longer touches AppKit (import dropped). The Settings scene activates the app and orders its window front, above the wizard. Co-Authored-By: Claude Fable 5 --- .../Screens/Onboarding/OnboardingSteps.swift | 9 ++++++++- .../Onboarding/OnboardingViewModel.swift | 17 ++++++++--------- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/macos/Codescribe/Screens/Onboarding/OnboardingSteps.swift b/macos/Codescribe/Screens/Onboarding/OnboardingSteps.swift index 0040b935..d6de3f8a 100644 --- a/macos/Codescribe/Screens/Onboarding/OnboardingSteps.swift +++ b/macos/Codescribe/Screens/Onboarding/OnboardingSteps.swift @@ -241,6 +241,12 @@ struct HotkeyModeStepView: View { struct AgenticReadinessStepView: View { @ObservedObject var model: OnboardingViewModel + // macOS 14+ action to open the app's Settings scene. This accessory / + // LSUIElement app has no responder for the private `showSettingsWindow:` + // selector, so the SwiftUI environment action is the only reliable open path + // (matching TrayMenuView / AgentChatView). The Settings scene activates the + // app and orders its window front, above the wizard. + @Environment(\.openSettings) private var openSettings var body: some View { VStack(alignment: .leading, spacing: 16) { @@ -298,7 +304,8 @@ struct AgenticReadinessStepView: View { .fixedSize(horizontal: false, vertical: true) HStack(spacing: 10) { OnboardingButton(title: "Set up MCP servers", kind: .primary) { - model.openMcpSettings() + model.prepareMcpSettingsDeepLink() + openSettings() } OnboardingButton(title: "Skip for now", kind: .secondary) { model.dismissMcpSetupPrompt() diff --git a/macos/Codescribe/Screens/Onboarding/OnboardingViewModel.swift b/macos/Codescribe/Screens/Onboarding/OnboardingViewModel.swift index b3b1d8ac..39dfc2c6 100644 --- a/macos/Codescribe/Screens/Onboarding/OnboardingViewModel.swift +++ b/macos/Codescribe/Screens/Onboarding/OnboardingViewModel.swift @@ -1,4 +1,3 @@ -import AppKit import SwiftUI // State machine for the first-run wizard: current step index (persisted on every @@ -217,15 +216,15 @@ final class OnboardingViewModel: ObservableObject { mcpStatus = agentStatus.mcpStatus() } - /// Route the user to the live MCP management surface (Settings › Engine) via a - /// one-shot deep-link, then open the standard Settings window. The wizard stays - /// open behind it, so the user can wire a server and return to continue. - func openMcpSettings() { + /// Arm the one-shot deep-link so the Settings window lands on the MCP surface + /// (Settings › Engine). The view owns the actual open via SwiftUI's + /// `@Environment(\.openSettings)` — the only reliable path in this accessory / + /// LSUIElement app, where the private `showSettingsWindow:` selector has no + /// responder (matching TrayMenuView / AgentChatView). Call this immediately + /// before `openSettings()`; the wizard stays open behind Settings so the user + /// can wire a server and return to continue. + func prepareMcpSettingsDeepLink() { SettingsDeepLink.pendingSection = .engine - if !NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil) { - // Older selector name kept as a defensive fallback. - _ = NSApp.sendAction(Selector(("showPreferencesWindow:")), to: nil, from: nil) - } } /// Dismiss the MCP setup prompt for this session so onboarding proceeds without