Files
openclaw/apps/macos/Sources/OpenClaw/OnboardingView+Actions.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

118 lines
4.3 KiB
Swift

import Foundation
import OpenClawDiscovery
import OpenClawIPC
import SwiftUI
extension OnboardingView {
func selectLocalGateway() {
if state.connectionMode != .local {
resetGatewayBoundAIState()
}
defaultsToLocalGateway = false
state.connectionMode = .local
preferredGatewayID = nil
showAdvancedConnection = false
showRemoteChoices = false
GatewayDiscoveryPreferences.setPreferredStableID(nil)
probeConfiguredGatewayForDashboard()
}
func selectUnconfiguredGateway() {
resetGatewayBoundAIState()
defaultsToLocalGateway = false
state.connectionMode = .unconfigured
preferredGatewayID = nil
showAdvancedConnection = false
showRemoteChoices = false
GatewayDiscoveryPreferences.setPreferredStableID(nil)
}
func selectRemoteGateway(_ gateway: GatewayDiscoveryModel.DiscoveredGateway) {
let shouldResetGatewayState = Self.shouldResetGatewayBoundAIState(
connectionMode: state.connectionMode,
currentPreferredGatewayID: self.effectivePreferredGatewayID,
persistedPreferredGatewayID: GatewayDiscoveryPreferences.preferredStableID(),
selectedGatewayID: gateway.stableID)
if shouldResetGatewayState {
// The mode can remain `.remote` while the selected Gateway changes,
// so its onChange hook alone cannot retire route-bound state.
resetGatewayBoundAIState()
resetRemoteProbeFeedback()
}
defaultsToLocalGateway = false
preferredGatewayID = gateway.stableID
GatewayDiscoverySelectionSupport.applyRemoteSelection(gateway: gateway, state: state)
state.connectionMode = .remote
MacNodeModeCoordinator.shared.setPreferredGatewayStableID(gateway.stableID, state: state)
probeConfiguredGatewayForDashboard()
}
static func shouldResetGatewayBoundAIState(
connectionMode: AppState.ConnectionMode,
currentPreferredGatewayID: String?,
persistedPreferredGatewayID: String?,
selectedGatewayID: String) -> Bool
{
let currentGatewayID = Self.normalizedGatewayID(currentPreferredGatewayID) ??
Self.normalizedGatewayID(persistedPreferredGatewayID)
return connectionMode != .remote || currentGatewayID != Self.normalizedGatewayID(selectedGatewayID)
}
private static func normalizedGatewayID(_ value: String?) -> String? {
let trimmed = value?.trimmingCharacters(in: .whitespacesAndNewlines)
return trimmed?.isEmpty == false ? trimmed : nil
}
var effectivePreferredGatewayID: String? {
let persisted = Self.normalizedGatewayID(GatewayDiscoveryPreferences.preferredStableID())
guard let local = Self.normalizedGatewayID(preferredGatewayID) else {
return persisted
}
// Config-watcher endpoint changes clear the persisted owner. Ignore the
// stale @State copy until the view's next render catches up.
return local == persisted ? local : persisted
}
func openSettings(tab: SettingsTab) {
AppNavigationActions.openSettings(tab: tab)
}
func handleBack() {
withAnimation {
self.currentPage = max(0, self.currentPage - 1)
}
}
func handleNext() {
// All callers (Next button, chat handoff) honor the same page gates.
guard canAdvance else { return }
self.commitRecommendedConnectionIfNeeded(for: activePageIndex)
if currentPage < pageCount - 1 {
withAnimation { self.currentPage += 1 }
} else {
self.finish()
}
}
func commitRecommendedConnectionIfNeeded(for pageIndex: Int) {
if pageIndex == connectionPageIndex,
defaultsToLocalGateway,
state.connectionMode == .unconfigured
{
self.selectLocalGateway()
}
}
func finish() {
aiSetup.clearCompletedHandoffIfOwned()
OnboardingController.markComplete()
OnboardingController.shared.close()
// Land people in the real conversation, not on an empty desktop: the
// agent chat is the product, and it is verified working by now.
if state.connectionMode != .unconfigured {
AppNavigationActions.openChat()
}
}
}