@@ -17,9 +17,13 @@ enum RemoteOnboardingProbeState: Equatable {
1717}
1818
1919@MainActor
20- final class OnboardingController {
20+ final class OnboardingController : NSObject , NSWindowDelegate {
2121 static let shared = OnboardingController ( )
2222 private var window : NSWindow ?
23+ /// Human description of work in flight ("Installing the Gateway…").
24+ /// While set, closing the window asks for confirmation instead of quitting
25+ /// setup mid-operation.
26+ var busyReason : String ?
2327
2428 static func markComplete( ) {
2529 UserDefaults . standard. set ( true , forKey: onboardingSeenKey)
@@ -47,6 +51,7 @@ final class OnboardingController {
4751 window. titlebarAppearsTransparent = true
4852 window. titleVisibility = . hidden
4953 window. isMovableByWindowBackground = true
54+ window. delegate = self
5055 window. center ( )
5156 DockIconManager . shared. temporarilyShowDock ( )
5257 window. makeKeyAndOrderFront ( nil )
@@ -55,6 +60,7 @@ final class OnboardingController {
5560 }
5661
5762 func close( ) {
63+ self . busyReason = nil
5864 self . window? . close ( )
5965 self . window = nil
6066 }
@@ -67,12 +73,39 @@ final class OnboardingController {
6773 self . close ( )
6874 self . show ( )
6975 }
76+
77+ func windowShouldClose( _ sender: NSWindow ) -> Bool {
78+ guard let busyReason else { return true }
79+ let alert = NSAlert ( )
80+ alert. messageText = " Setup is still working "
81+ alert. informativeText =
82+ " \( busyReason) \n \n You can keep this window open until it finishes, " +
83+ " or quit setup and pick it up again later from the menu bar. "
84+ alert. alertStyle = . warning
85+ alert. addButton ( withTitle: " Continue Setup " )
86+ alert. addButton ( withTitle: " Quit Setup " )
87+ let response = alert. runModal ( )
88+ return response == . alertSecondButtonReturn
89+ }
90+
91+ func windowWillClose( _ notification: Notification ) {
92+ guard let closing = notification. object as? NSWindow , closing === self . window else { return }
93+ self . busyReason = nil
94+ self . window = nil
95+ }
7096}
7197
7298struct OnboardingView : View {
99+ enum CLIInstallPhase {
100+ case idle
101+ case installing
102+ case startingService
103+ }
104+
73105 @State var currentPage = 0
74106 @State var isRequesting = false
75107 @State var installingCLI = false
108+ @State var cliInstallPhase : CLIInstallPhase = . idle
76109 @State var cliStatus : String ?
77110 @State var copied = false
78111 @State var monitoringPermissions = false
@@ -87,6 +120,7 @@ struct OnboardingView: View {
87120 @State var needsBootstrap = false
88121 @State var didAutoKickoff = false
89122 @State var showAdvancedConnection = false
123+ @State var showRemoteChoices = false
90124 @State var preferredGatewayID : String ?
91125 @State var remoteProbeState : RemoteOnboardingProbeState = . idle
92126 @State var remoteAuthIssue : RemoteGatewayAuthIssue ?
@@ -95,7 +129,7 @@ struct OnboardingView: View {
95129 @State var onboardingChatModel : OpenClawChatViewModel
96130 @State var onboardingSkillsModel = SkillsSettingsModel ( )
97131 @State var crestodianChat = CrestodianOnboardingChatModel ( )
98- @State var crestodianSetupComplete = false
132+ @State var aiSetup = OnboardingAISetupModel ( )
99133 @State var didLoadOnboardingSkills = false
100134 @State var localGatewayProbe : LocalGatewayProbe ?
101135 @State var defaultsToLocalGateway : Bool
@@ -106,15 +140,32 @@ struct OnboardingView: View {
106140 static let windowHeight : CGFloat = 752 // ~+10% to fit full onboarding content
107141
108142 let pageWidth : CGFloat = Self . windowWidth
109- // Sized so the permissions page fits all capabilities without scrolling:
110- // 145 (icon header) + 535 + ~60 (nav bar) stays inside windowHeight 752.
111- let contentHeight : CGFloat = 535
112143 let connectionPageIndex = 1
113144 let cliPageIndex = 2
114- let crestodianPageIndex = 3
145+ let aiPageIndex = 3
115146 let onboardingChatPageIndex = 8
116147
117148 let permissionsPageIndex = 5
149+
150+ /// Chat-like pages shrink the mascot so the conversation gets the room.
151+ var usesCompactHero : Bool {
152+ [ self . aiPageIndex, self . onboardingChatPageIndex] . contains ( self . activePageIndex)
153+ }
154+
155+ var heroFrameHeight : CGFloat {
156+ self . usesCompactHero ? 78 : 145
157+ }
158+
159+ var heroSize : CGFloat {
160+ self . usesCompactHero ? 64 : 130
161+ }
162+
163+ /// Sized so the permissions page fits all capabilities without scrolling:
164+ /// heroFrameHeight + contentHeight + ~72 (nav bar) fills windowHeight 752.
165+ var contentHeight : CGFloat {
166+ Self . windowHeight - self . heroFrameHeight - 72
167+ }
168+
118169 static func pageOrder(
119170 for mode: AppState . ConnectionMode ,
120171 showOnboardingChat: Bool ,
@@ -123,8 +174,9 @@ struct OnboardingView: View {
123174 switch mode {
124175 case . remote:
125176 // Remote setup doesn't need local gateway/CLI/workspace setup pages,
126- // and WhatsApp/Telegram setup is optional.
127- return showOnboardingChat ? [ 0 , 1 , 5 , 8 , 9 ] : [ 0 , 1 , 5 , 9 ]
177+ // but the AI check runs against the remote gateway so a broken
178+ // remote model surfaces here, not in the first chat.
179+ return showOnboardingChat ? [ 0 , 1 , 3 , 5 , 8 , 9 ] : [ 0 , 1 , 3 , 5 , 9 ]
128180 case . unconfigured:
129181 return showOnboardingChat ? [ 0 , 1 , 8 , 9 ] : [ 0 , 1 , 9 ]
130182 case . local:
@@ -171,18 +223,18 @@ struct OnboardingView: View {
171223 self . activePageIndex == self . cliPageIndex && !self . cliInstalled
172224 }
173225
174- /// Local onboarding must not finish with nothing configured : the Crestodian
175- /// page blocks Next until setup authored the config (the conversation's
176- /// "yes"), mirroring the old wizard-completion gate . "Configure later" on
177- /// the connection page remains the explicit skip path.
178- var isCrestodianBlocking : Bool {
179- self . activePageIndex == self . crestodianPageIndex &&
180- self . state. connectionMode == . local &&
181- !self . crestodianSetupComplete
226+ /// Onboarding must not finish without working inference : the AI page
227+ /// blocks Next until a candidate passed its live test (config is authored
228+ /// server-side on that success) . "Configure later" on the connection page
229+ /// remains the explicit skip path.
230+ var isAISetupBlocking : Bool {
231+ self . activePageIndex == self . aiPageIndex &&
232+ self . state. connectionMode != . unconfigured &&
233+ !self . aiSetup . connected
182234 }
183235
184236 var canAdvance : Bool {
185- !self . isCLIBlocking && !self . isCrestodianBlocking
237+ !self . isCLIBlocking && !self . isAISetupBlocking
186238 }
187239
188240 struct LocalGatewayProbe : Equatable {
0 commit comments