Files
openclaw/apps/macos/Sources/OpenClaw/OnboardingView+AISetupPage.swift
Peter Steinberger 96f0983a85 fix(onboarding): skip setup for configured gateways and require inference first (#102883)
* fix(crestodian): keep onboarding RPCs restart-safe

* fix(profiles): isolate approval state migrations

* fix(crestodian): bypass configured gateway setup

* test(crestodian): type onboarding mocks

* fix(onboarding): require inference before Crestodian

* fix(onboarding): enforce verified inference handoff

* fix(macos): reset setup on gateway endpoint edits

* chore(i18n): refresh native source inventory

* fix(gateway): keep socket on request cancellation

* test(packaging): require workspace templates

* fix(onboarding): bind setup to verified inference

* fix(onboarding): align inference gate contracts

* fix(crestodian): classify concurrent policy rejection

* test(crestodian): expect registry restoration

* fix(onboarding): bind setup to configured gateways

* fix(codex): preserve startup phase deadlines

* test(crestodian): match fail-closed policy ordering

* test(onboarding): assert bound gateway handoff

* fix(codex): bind runtime resolution to spawn cwd

* test(crestodian): assert policy rejection order

* fix(cli): preserve gateway routing across restarts

* fix(macos): fail closed during gateway edits

* test(macos): cover gateway route generation races

* chore: keep release notes out of onboarding PR

* fix(ci): refresh onboarding generated checks

* style(swift): align gateway channel formatting

* fix(ci): refresh plugin SDK surface budgets

* fix(ci): resync native string inventory

* refactor(swift): split gateway channel support

* test(doctor): isolate plugin compatibility registry

* test(macos): isolate gateway onboarding fixtures

* test(macos): assert gateway lease health ordering

* fix(codex): reconcile computer-use startup changes
2026-07-11 10:25:14 -07:00

126 lines
5.3 KiB
Swift

import SwiftUI
extension OnboardingView {
/// Structured AI setup: detect what's already on this machine, test the
/// best option live, fall through automatically, offer an API-key form
/// when nothing works. Crestodian becomes available only after inference
/// has completed a live round-trip.
func aiSetupPage(contentHeight: CGFloat) -> some View {
VStack(spacing: 12) {
Text("Connect your AI")
.font(.largeTitle.weight(.semibold))
Text(self.aiSetupSubtitle)
.font(.body)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
.frame(maxWidth: 540)
.fixedSize(horizontal: false, vertical: true)
ScrollView {
OnboardingAISetupView(
model: self.aiSetup,
crestodianChat: self.crestodianState.chat,
showCrestodianChat: self.$crestodianState.isPresented,
retryConfiguredGatewayProbe: { self.retryConfiguredGatewayProbe() })
.padding(.vertical, 4)
.padding(.trailing, 12)
}
.scrollIndicators(.automatic)
}
.padding(.horizontal, 28)
.padding(.top, 48)
.frame(width: self.pageWidth, height: contentHeight, alignment: .top)
}
private var aiSetupSubtitle: String {
if aiSetup.connected {
return "All good — your assistant has a working AI connection."
}
return "OpenClaw needs an AI account to think. " +
"It reuses what you already have — nothing new to sign up for if " +
"Claude Code, Codex, or an API key is on this Mac."
}
func maybeStartAISetup(for pageIndex: Int) {
guard pageIndex == aiPageIndex else { return }
// Local mode reaches this page only after the CLI/gateway install page,
// so the gateway is up before the first RPC.
guard state.connectionMode != .local || cliInstalled else { return }
self.prepareCrestodianHandoff()
// A selected/reconnected Gateway may already have a configured default
// agent. Check that route before setup tries to author inference.
probeConfiguredGatewayForDashboard(startAISetupWhenMissing: true)
}
func prepareCrestodianHandoff() {
crestodianState.chat.onAgentHandoff = { [self] in self.finish() }
aiSetup.onPendingActivationDeadline = { [self] deadline, routeIdentity in
let currentRouteIdentity = self.aiSetupRouteIdentityProvider()
guard currentRouteIdentity == routeIdentity else { return }
self.configuredGatewayProbe.schedulePendingActivationRecheck(deadline: deadline) {
self.probeConfiguredGatewayForDashboard(startAISetupWhenMissing: true)
}
}
if aiSetup.onConnected == nil {
aiSetup.onConnected = { [self] in
// Activation already persisted the resume marker before its RPC.
self.configuredGatewayProbe.cancelPendingActivationRecheck()
self.crestodianState.presentAndStart()
}
}
}
@discardableResult
func resumePendingCrestodian(modelRef: String) -> Task<Void, Never> {
self.prepareCrestodianHandoff()
let expectedRouteIdentity = self.aiSetupRouteIdentityProvider()
aiSetup.resumeConfiguredInference(modelRef: modelRef)
if let page = pageOrder.firstIndex(of: aiPageIndex) {
currentPage = page
}
return Task {
let outcome = await self.aiSetup.verifyPendingConfiguredInference()
// The outcome belongs to the exact attempt and route captured by
// verification. Never infer success from newer mutable UI state.
let currentRouteIdentity = self.aiSetupRouteIdentityProvider()
guard outcome == .connected,
self.aiSetup.connected,
currentRouteIdentity == expectedRouteIdentity,
!Task.isCancelled
else { return }
self.configuredGatewayProbe.cancelPendingActivationRecheck()
// `onConnected` already owns presentation. Await that exact start
// task without starting a replacement route's chat after suspension.
await self.crestodianState.waitForStartIfNeeded()
}
}
func waitForPendingInferenceSetup() {
self.prepareCrestodianHandoff()
if let page = pageOrder.firstIndex(of: aiPageIndex) {
currentPage = page
}
aiSetup.waitForPendingActivationDeadline()
}
@discardableResult
func retryConfiguredGatewayProbe() -> Task<Void, Never>? {
aiSetup.beginConfiguredGatewayProbeRetry()
// The retry button itself proves the onboarding view is visible even
// before SwiftUI commits an @State visibility write.
return probeConfiguredGatewayForDashboard(
startAISetupWhenMissing: true,
knownVisible: true,
knownAISetupPage: true)
}
func resumePendingInferenceSetup() {
self.prepareCrestodianHandoff()
if let page = pageOrder.firstIndex(of: aiPageIndex) {
currentPage = page
}
aiSetup.resetForGatewayChange(clearPendingHandoff: false)
aiSetup.startIfNeeded()
}
}