fix(macos): keep onboarding green across gateway plugin restart (#101218)

* fix(macos): keep onboarding green across gateway plugin restart

Activating the Codex candidate installs the codex plugin and the gateway
restarts itself to load it, dropping the activate RPC socket after the
server already persisted success. Onboarding then claimed every option
failed. Reconcile transport drops against crestodian.setup.detect before
reporting failure, and stop latching the finish-page skills load error
that fires while the pager pre-builds pages before the gateway exists.

* chore(i18n): refresh native inventory line numbers
This commit is contained in:
Peter Steinberger
2026-07-07 00:32:34 +01:00
committed by GitHub
parent 91d4e8ed8e
commit c5161f96ce
3 changed files with 78 additions and 31 deletions

View File

@@ -18475,7 +18475,7 @@
},
{
"kind": "ui-call",
"line": 377,
"line": 416,
"path": "apps/macos/Sources/OpenClaw/OnboardingAISetup.swift",
"source": "Looking for AI you already use…",
"surface": "apple",
@@ -18483,7 +18483,7 @@
},
{
"kind": "ui-call",
"line": 379,
"line": 418,
"path": "apps/macos/Sources/OpenClaw/OnboardingAISetup.swift",
"source": "Checking for Claude Code, Codex, Gemini, and saved API keys.",
"surface": "apple",
@@ -18491,7 +18491,7 @@
},
{
"kind": "ui-named-argument",
"line": 409,
"line": 448,
"path": "apps/macos/Sources/OpenClaw/OnboardingAISetup.swift",
"source": "Couldnt check this Mac for AI accounts",
"surface": "apple",
@@ -18499,7 +18499,7 @@
},
{
"kind": "ui-named-argument",
"line": 420,
"line": 459,
"path": "apps/macos/Sources/OpenClaw/OnboardingAISetup.swift",
"source": "Couldnt load the full provider list",
"surface": "apple",
@@ -18507,7 +18507,7 @@
},
{
"kind": "ui-named-argument",
"line": 423,
"line": 462,
"path": "apps/macos/Sources/OpenClaw/OnboardingAISetup.swift",
"source": "Try again",
"surface": "apple",
@@ -18515,7 +18515,7 @@
},
{
"kind": "ui-named-argument",
"line": 431,
"line": 470,
"path": "apps/macos/Sources/OpenClaw/OnboardingAISetup.swift",
"source": "None of the found options worked",
"surface": "apple",
@@ -18523,7 +18523,7 @@
},
{
"kind": "ui-named-argument",
"line": 432,
"line": 471,
"path": "apps/macos/Sources/OpenClaw/OnboardingAISetup.swift",
"source": "The details are listed on each option above. You can fix the login and retry, or connect with an API key or token below.",
"surface": "apple",
@@ -18531,7 +18531,7 @@
},
{
"kind": "ui-call",
"line": 449,
"line": 488,
"path": "apps/macos/Sources/OpenClaw/OnboardingAISetup.swift",
"source": "Need help? Chat with Crestodian",
"surface": "apple",
@@ -18539,7 +18539,7 @@
},
{
"kind": "ui-call",
"line": 462,
"line": 501,
"path": "apps/macos/Sources/OpenClaw/OnboardingAISetup.swift",
"source": "Your AI is ready",
"surface": "apple",
@@ -18547,7 +18547,7 @@
},
{
"kind": "ui-call",
"line": 479,
"line": 518,
"path": "apps/macos/Sources/OpenClaw/OnboardingAISetup.swift",
"source": "No AI accounts found on this Mac",
"surface": "apple",
@@ -18555,7 +18555,7 @@
},
{
"kind": "ui-call-concatenated",
"line": 481,
"line": 520,
"path": "apps/macos/Sources/OpenClaw/OnboardingAISetup.swift",
"source": "Thats fine — you can connect one with an API key or token. If you use Claude Code, Codex, or the Gemini CLI on this Mac, sign in there first and hit “Check again”.",
"surface": "apple",
@@ -18563,7 +18563,7 @@
},
{
"kind": "ui-call",
"line": 513,
"line": 552,
"path": "apps/macos/Sources/OpenClaw/OnboardingAISetup.swift",
"source": "Recommended",
"surface": "apple",
@@ -18571,7 +18571,7 @@
},
{
"kind": "ui-named-argument",
"line": 596,
"line": 635,
"path": "apps/macos/Sources/OpenClaw/OnboardingAISetup.swift",
"source": "No key-based providers are available",
"surface": "apple",
@@ -18579,7 +18579,7 @@
},
{
"kind": "ui-named-argument",
"line": 597,
"line": 636,
"path": "apps/macos/Sources/OpenClaw/OnboardingAISetup.swift",
"source": "Enable or install a text-inference provider plugin on this Gateway, then check again.",
"surface": "apple",
@@ -18587,7 +18587,7 @@
},
{
"kind": "ui-named-argument",
"line": 599,
"line": 638,
"path": "apps/macos/Sources/OpenClaw/OnboardingAISetup.swift",
"source": "Check again",
"surface": "apple",
@@ -18595,7 +18595,7 @@
},
{
"kind": "ui-call",
"line": 611,
"line": 650,
"path": "apps/macos/Sources/OpenClaw/OnboardingAISetup.swift",
"source": "Connect with an API key or token instead…",
"surface": "apple",
@@ -18603,7 +18603,7 @@
},
{
"kind": "ui-call",
"line": 622,
"line": 661,
"path": "apps/macos/Sources/OpenClaw/OnboardingAISetup.swift",
"source": "Connect with an API key or token",
"surface": "apple",
@@ -18611,7 +18611,7 @@
},
{
"kind": "ui-call",
"line": 625,
"line": 664,
"path": "apps/macos/Sources/OpenClaw/OnboardingAISetup.swift",
"source": "Provider",
"surface": "apple",
@@ -18619,7 +18619,7 @@
},
{
"kind": "ui-call",
"line": 633,
"line": 672,
"path": "apps/macos/Sources/OpenClaw/OnboardingAISetup.swift",
"source": "API key or token",
"surface": "apple",
@@ -18627,7 +18627,7 @@
},
{
"kind": "ui-call",
"line": 645,
"line": 684,
"path": "apps/macos/Sources/OpenClaw/OnboardingAISetup.swift",
"source": "Connect",
"surface": "apple",
@@ -18635,7 +18635,7 @@
},
{
"kind": "ui-named-argument",
"line": 658,
"line": 697,
"path": "apps/macos/Sources/OpenClaw/OnboardingAISetup.swift",
"source": "That key didnt work",
"surface": "apple",
@@ -18643,7 +18643,7 @@
},
{
"kind": "ui-call",
"line": 683,
"line": 722,
"path": "apps/macos/Sources/OpenClaw/OnboardingAISetup.swift",
"source": "Crestodian — setup helper",
"surface": "apple",
@@ -18651,7 +18651,7 @@
},
{
"kind": "ui-call",
"line": 686,
"line": 725,
"path": "apps/macos/Sources/OpenClaw/OnboardingAISetup.swift",
"source": "Done",
"surface": "apple",
@@ -18659,7 +18659,7 @@
},
{
"kind": "ui-call",
"line": 727,
"line": 766,
"path": "apps/macos/Sources/OpenClaw/OnboardingAISetup.swift",
"source": "Open help…",
"surface": "apple",
@@ -19459,7 +19459,7 @@
},
{
"kind": "ui-call",
"line": 1000,
"line": 1008,
"path": "apps/macos/Sources/OpenClaw/OnboardingView+Pages.swift",
"source": "Skills included",
"surface": "apple",
@@ -19467,7 +19467,7 @@
},
{
"kind": "ui-call",
"line": 1007,
"line": 1015,
"path": "apps/macos/Sources/OpenClaw/OnboardingView+Pages.swift",
"source": "Refresh",
"surface": "apple",
@@ -19475,7 +19475,7 @@
},
{
"kind": "ui-call",
"line": 1016,
"line": 1024,
"path": "apps/macos/Sources/OpenClaw/OnboardingView+Pages.swift",
"source": "Couldnt load skills from the Gateway.",
"surface": "apple",
@@ -19483,7 +19483,7 @@
},
{
"kind": "ui-call-concatenated",
"line": 1019,
"line": 1027,
"path": "apps/macos/Sources/OpenClaw/OnboardingView+Pages.swift",
"source": "Make sure the Gateway is running and connected, then hit Refresh (or open Settings → Skills).",
"surface": "apple",
@@ -19491,7 +19491,7 @@
},
{
"kind": "ui-call",
"line": 1025,
"line": 1033,
"path": "apps/macos/Sources/OpenClaw/OnboardingView+Pages.swift",
"source": "Details: \\(error)",
"surface": "apple",
@@ -19499,7 +19499,7 @@
},
{
"kind": "ui-call",
"line": 1031,
"line": 1039,
"path": "apps/macos/Sources/OpenClaw/OnboardingView+Pages.swift",
"source": "No skills reported yet.",
"surface": "apple",

View File

@@ -243,12 +243,51 @@ final class OnboardingAISetupModel {
await self.tryNextAfterFailure(of: kind)
}
} catch {
guard token == self.attemptToken else { return }
// Activating a CLI candidate can install a provider plugin (Codex),
// and the gateway restarts itself to load it dropping this RPC's
// socket after the server already tested and persisted the model.
// A transport error means "outcome unknown", not "failed": re-read
// server state before reporting failure.
if await self.reconcileActivationAfterTransportDrop(kind: kind, token: token) { return }
guard token == self.attemptToken else { return }
self.statuses[kind] = .failed(message: Self.friendlyTransportError(error.localizedDescription))
await self.tryNextAfterFailure(of: kind)
}
}
/// After a transport drop during activate, poll `crestodian.setup.detect`
/// (the gateway restart takes a few seconds) and count the attempt as
/// connected only when the server persisted exactly the model this
/// candidate would have written. Returns true when reconciled.
private func reconcileActivationAfterTransportDrop(kind: String, token: UUID) async -> Bool {
guard let expected = self.candidates.first(where: { $0.kind == kind })?.modelRef else {
return false
}
for delayMs in [2000, 4000, 6000] {
try? await Task.sleep(nanoseconds: UInt64(delayMs) * 1_000_000)
guard token == self.attemptToken else { return false }
guard let data = try? await GatewayConnection.shared.request(
method: "crestodian.setup.detect",
params: [:],
timeoutMs: 10000,
retryTransportFailures: true)
else { continue }
guard token == self.attemptToken else { return false }
guard let result = try? JSONDecoder().decode(DetectResult.self, from: data) else { return false }
if result.setupComplete, result.configuredModel == expected {
self.finishConnected(
kind: kind,
result: ActivateResult(ok: true, modelRef: expected, latencyMs: nil, status: nil, error: nil))
return true
}
// The gateway answered and setup is not complete: the activation
// genuinely failed before persisting report the original error.
return false
}
return false
}
func submitManualKey() {
let key = self.manualKey.trimmingCharacters(in: .whitespacesAndNewlines)
guard let provider = self.selectedManualProvider, !key.isEmpty, !self.manualTesting else { return }

View File

@@ -983,10 +983,18 @@ extension OnboardingView {
}
}
.task { await self.maybeLoadOnboardingSkills() }
.onChange(of: self.currentPage) { _, newValue in
// The pager builds every page up front, so the initial load above
// can run before the local gateway is configured and fail. Retry
// when the user actually lands here instead of latching the error.
guard self.activePageIndex(for: newValue) == self.pageOrder.last else { return }
Task { await self.maybeLoadOnboardingSkills() }
}
}
private func maybeLoadOnboardingSkills() async {
guard !self.didLoadOnboardingSkills else { return }
if self.onboardingSkillsModel.isLoading { return }
if self.didLoadOnboardingSkills, self.onboardingSkillsModel.error == nil { return }
self.didLoadOnboardingSkills = true
await self.onboardingSkillsModel.refresh()
}