Files
openclaw/apps/macos/Sources/OpenClaw/OnboardingView+Actions.swift
Hannes Rudolph 34c3d15a6b fix(macos): complete Codex onboarding and model picker (#124829)
* fix(setup): refresh Codex registry with staged install

* fix(macos): verify inference before onboarding handoff

* fix(setup): use native Codex home for subscription auth

* fix(codex): honor attempt-scoped setup config

* fix(macos): align onboarding handoff with reopen

* fix(setup): await prepared model convergence

* fix(ui): avoid false auth state for empty catalog

* fix(setup): scope catalog convergence to Codex gateway

* fix(setup): publish the committed runtime catalog

* fix(models): project configured static runtime models

* fix(codex): expose app-server model catalog

* fix(models): preserve Codex auth across reloads

* fix(ci): align Codex onboarding checks

* test(ui): stabilize dock suppression environment

* fix(codex): honor discovery config in app-server model catalog

The manifest documents discovery.enabled (bundled fallback list) and
discovery.timeoutMs (default 2500ms) for model discovery; the new catalog
path used the generic 60s request timeout and ignored the enable gate.
Also drop the test-only listModels injection seam in favor of vi.mock.

* fix(setup): refuse prepared Codex auth over an explicit remote transport

configureCodexCliPreparedAuth silently rewrote an explicitly configured
websocket/unix app-server to local stdio (keeping a dangling url), moving
the credential boundary onto this host. Fail setup with actionable
guidance instead; also surface the root cause when the prepared model
catalog refresh fails after activation.

* refactor(agents): one canonical model-catalog identity key

Three near-identical key helpers existed (models-list-result,
models-list-configured-static, harness/model-catalog). Export
resolveModelCatalogIdentityKey from the route-policy owner, collapse the
duplicate dedupe loops into dedupeByKey, make donor enrichment Map-based,
and inline the one-off harness-augment wrapper.

* fix(macos): restore custodian handoff for fresh activations

Landing every finish on the plain dashboard stranded the custodian
first-run flow (memory import, channels, permissions, hatch). Fresh
activations now hand off to custodian onboarding; live-verified
pre-existing setups reopen the normal dashboard, matching the removed
already-configured shortcut. Tests pin the destination per path.

Also isolate the post-startup Codex login test from developer machines:
ambient OPENAI_API_KEY and a real Codex login made it assert-fail.

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-08-17 23:31:12 -07:00

133 lines
5.1 KiB
Swift

import Foundation
import OpenClawDiscovery
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() {
guard canAdvance else { return }
let remoteDecision = Self.remoteGatewayAdvanceDecision(
connectionMode: state.connectionMode,
activePageIndex: activePageIndex,
connectionPageIndex: connectionPageIndex,
authIssue: remoteAuthIssue,
probeState: remoteProbeState,
input: remoteGatewayProbeInput)
guard remoteDecision.canAdvance else {
if remoteDecision.shouldProbe {
Task { await self.probeRemoteConnection(advanceOnSuccess: true) }
}
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()
}
}
@discardableResult
func finish() -> Bool {
guard !finishState.didFinish else { return false }
finishState.didFinish = true
aiSetup.clearCompletedHandoffIfOwned()
OnboardingController.markComplete()
OnboardingController.shared.close()
guard state.connectionMode != .unconfigured else { return true }
// Fresh activation hands off to the dashboard's custodian onboarding, which
// owns the remaining first-run steps (memory import, channels, permissions,
// hatch). A live-verified pre-existing setup reopens the normal dashboard.
dashboardHandoffOpener(aiSetup.verifiedExistingInference ? .dashboard : .custodianOnboarding)
return true
}
}