Files
openclaw/apps/macos/Sources/OpenClaw/Onboarding.swift
Nimrod Gutman 144c1b802b macOS/onboarding: prompt for remote gateway auth tokens (#43100)
Merged via squash.

Prepared head SHA: 00e2ad847b
Co-authored-by: ngutman <1540134+ngutman@users.noreply.github.com>
Co-authored-by: ngutman <1540134+ngutman@users.noreply.github.com>
Reviewed-by: @ngutman
2026-03-11 13:53:19 +02:00

180 lines
5.8 KiB
Swift

import AppKit
import Observation
import OpenClawChatUI
import OpenClawDiscovery
import OpenClawIPC
import SwiftUI
enum UIStrings {
static let welcomeTitle = "Welcome to OpenClaw"
}
enum RemoteOnboardingProbeState: Equatable {
case idle
case checking
case ok(RemoteGatewayProbeSuccess)
case failed(String)
}
@MainActor
final class OnboardingController {
static let shared = OnboardingController()
private var window: NSWindow?
func show() {
if ProcessInfo.processInfo.isNixMode {
// Nix mode is fully declarative; onboarding would suggest interactive setup that doesn't apply.
UserDefaults.standard.set(true, forKey: "openclaw.onboardingSeen")
UserDefaults.standard.set(currentOnboardingVersion, forKey: onboardingVersionKey)
AppStateStore.shared.onboardingSeen = true
return
}
if let window {
DockIconManager.shared.temporarilyShowDock()
window.makeKeyAndOrderFront(nil)
NSApp.activate(ignoringOtherApps: true)
return
}
let hosting = NSHostingController(rootView: OnboardingView())
let window = NSWindow(contentViewController: hosting)
window.title = UIStrings.welcomeTitle
window.setContentSize(NSSize(width: OnboardingView.windowWidth, height: OnboardingView.windowHeight))
window.styleMask = [.titled, .closable, .fullSizeContentView]
window.titlebarAppearsTransparent = true
window.titleVisibility = .hidden
window.isMovableByWindowBackground = true
window.center()
DockIconManager.shared.temporarilyShowDock()
window.makeKeyAndOrderFront(nil)
NSApp.activate(ignoringOtherApps: true)
self.window = window
}
func close() {
self.window?.close()
self.window = nil
}
func restart() {
self.close()
self.show()
}
}
struct OnboardingView: View {
@Environment(\.openSettings) var openSettings
@State var currentPage = 0
@State var isRequesting = false
@State var installingCLI = false
@State var cliStatus: String?
@State var copied = false
@State var monitoringPermissions = false
@State var monitoringDiscovery = false
@State var cliInstalled = false
@State var cliInstallLocation: String?
@State var workspacePath: String = ""
@State var workspaceStatus: String?
@State var workspaceApplying = false
@State var needsBootstrap = false
@State var didAutoKickoff = false
@State var showAdvancedConnection = false
@State var preferredGatewayID: String?
@State var remoteProbeState: RemoteOnboardingProbeState = .idle
@State var remoteAuthIssue: RemoteGatewayAuthIssue?
@State var suppressRemoteProbeReset = false
@State var gatewayDiscovery: GatewayDiscoveryModel
@State var onboardingChatModel: OpenClawChatViewModel
@State var onboardingSkillsModel = SkillsSettingsModel()
@State var onboardingWizard = OnboardingWizardModel()
@State var didLoadOnboardingSkills = false
@State var localGatewayProbe: LocalGatewayProbe?
@Bindable var state: AppState
var permissionMonitor: PermissionMonitor
static let windowWidth: CGFloat = 630
static let windowHeight: CGFloat = 752 // ~+10% to fit full onboarding content
let pageWidth: CGFloat = Self.windowWidth
let contentHeight: CGFloat = 460
let connectionPageIndex = 1
let wizardPageIndex = 3
let onboardingChatPageIndex = 8
let permissionsPageIndex = 5
static func pageOrder(
for mode: AppState.ConnectionMode,
showOnboardingChat: Bool) -> [Int]
{
switch mode {
case .remote:
// Remote setup doesn't need local gateway/CLI/workspace setup pages,
// and WhatsApp/Telegram setup is optional.
showOnboardingChat ? [0, 1, 5, 8, 9] : [0, 1, 5, 9]
case .unconfigured:
showOnboardingChat ? [0, 1, 8, 9] : [0, 1, 9]
case .local:
showOnboardingChat ? [0, 1, 3, 5, 8, 9] : [0, 1, 3, 5, 9]
}
}
var showOnboardingChat: Bool {
self.state.connectionMode == .local && self.needsBootstrap
}
var pageOrder: [Int] {
Self.pageOrder(for: self.state.connectionMode, showOnboardingChat: self.showOnboardingChat)
}
var pageCount: Int {
self.pageOrder.count
}
var activePageIndex: Int {
self.activePageIndex(for: self.currentPage)
}
var buttonTitle: String {
self.currentPage == self.pageCount - 1 ? "Finish" : "Next"
}
var wizardPageOrderIndex: Int? {
self.pageOrder.firstIndex(of: self.wizardPageIndex)
}
var isWizardBlocking: Bool {
self.activePageIndex == self.wizardPageIndex && !self.onboardingWizard.isComplete
}
var canAdvance: Bool {
!self.isWizardBlocking
}
var devLinkCommand: String {
let version = GatewayEnvironment.expectedGatewayVersionString() ?? "latest"
return "npm install -g openclaw@\(version)"
}
struct LocalGatewayProbe: Equatable {
let port: Int
let pid: Int32
let command: String
let expected: Bool
}
init(
state: AppState = AppStateStore.shared,
permissionMonitor: PermissionMonitor = .shared,
discoveryModel: GatewayDiscoveryModel = GatewayDiscoveryModel(
localDisplayName: InstanceIdentity.displayName,
filterLocalGateways: false))
{
self.state = state
self.permissionMonitor = permissionMonitor
self._gatewayDiscovery = State(initialValue: discoveryModel)
self._onboardingChatModel = State(
initialValue: OpenClawChatViewModel(
sessionKey: "onboarding",
transport: MacGatewayChatTransport()))
}
}