Skip to content

Commit 2bc50d0

Browse files
authored
feat: verify AI access during macOS onboarding before the first chat (#100288)
* feat(crestodian): add live-tested structured inference setup (detect/activate gateway RPCs) * feat(macos): redesign onboarding around a verified Connect-your-AI step * docs: describe the verified AI onboarding step and gemini setup ladder entry * chore(macos): drop replaced OnboardingView+CrestodianSetup source * fix(macos): keep the AI-detect error card from pairing with an unproven empty-state claim * chore(protocol): regenerate Swift gateway models for crestodian.setup methods * test(crestodian): give setup-inference mocks explicit params for test-types lane * chore(i18n): sync native app string inventory for onboarding redesign * chore(i18n): sync native app string inventory for onboarding redesign
1 parent a5fdc07 commit 2bc50d0

31 files changed

Lines changed: 2672 additions & 445 deletions

apps/.i18n/native-source.json

Lines changed: 461 additions & 173 deletions
Large diffs are not rendered by default.
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import SwiftUI
2+
3+
/// Settings pane hosting the Crestodian setup/repair chat.
4+
///
5+
/// Crestodian answers even when no model is configured (deterministic engine
6+
/// on the gateway), so this pane is the "always works" place to fix config,
7+
/// switch models, connect channels, or run doctor — in plain language.
8+
struct CrestodianSettings: View {
9+
let isActive: Bool
10+
@State private var chat = CrestodianOnboardingChatModel(
11+
welcomeVariant: nil,
12+
sessionPrefix: "mac-settings-crestodian")
13+
14+
var body: some View {
15+
VStack(alignment: .leading, spacing: 20) {
16+
SettingsPageHeader(
17+
title: "Crestodian",
18+
subtitle: "Your setup helper. It can check status, fix config, switch models, " +
19+
"and connect channels — even when the agent itself is not working.")
20+
21+
SettingsCardGroup("Chat") {
22+
CrestodianOnboardingChatView(model: self.chat)
23+
.frame(maxWidth: .infinity, minHeight: 320, maxHeight: .infinity)
24+
}
25+
.frame(maxHeight: .infinity)
26+
27+
Text("Tip: try “status”, “doctor”, “set default model …”, or “connect telegram”.")
28+
.font(.caption)
29+
.foregroundStyle(.secondary)
30+
}
31+
.settingsDetailContent()
32+
.task(id: self.isActive) {
33+
guard self.isActive else { return }
34+
self.chat.onAgentHandoff = {
35+
AppNavigationActions.openChat()
36+
}
37+
await self.chat.startIfNeeded()
38+
}
39+
}
40+
}

apps/macos/Sources/OpenClaw/Onboarding.swift

Lines changed: 69 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -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\nYou 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

7298
struct 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

Comments
 (0)