diff --git a/src/OpenClaw.SetupEngine.UI/Pages/AiSetupPage.xaml.cs b/src/OpenClaw.SetupEngine.UI/Pages/AiSetupPage.xaml.cs index 09923f533d..20bdaef38f 100644 --- a/src/OpenClaw.SetupEngine.UI/Pages/AiSetupPage.xaml.cs +++ b/src/OpenClaw.SetupEngine.UI/Pages/AiSetupPage.xaml.cs @@ -878,6 +878,7 @@ private async Task RunCoreAsync(Func action, bool submi _request.Dispose(); _request = new(); var token = _request.Token; + var reportedFailure = false; _busy = true; _activities.Clear(); _activityKey = null; @@ -923,6 +924,7 @@ private async Task RunCoreAsync(Func action, bool submi if (Client?.Phase is not (GatewayAiSetupPhase.Running or GatewayAiSetupPhase.Uncertain or GatewayAiSetupPhase.VerificationRequired or GatewayAiSetupPhase.Prepared)) _providerOperationActive = false; + reportedFailure = true; ReportFailure(ex); _activityStatus = SetupInstallationStatus.Failed; } @@ -939,7 +941,7 @@ private async Task RunCoreAsync(Func action, bool submi if (Client?.Phase is GatewayAiSetupPhase.Cancelled or GatewayAiSetupPhase.Rejected) { _providerOperationActive = false; - if (Client.Phase == GatewayAiSetupPhase.Rejected) + if (Client.Phase == GatewayAiSetupPhase.Rejected && !reportedFailure) ShowError("Rejected"); } Render(); @@ -1213,12 +1215,21 @@ private bool CanConnectManual() => private void Preference_Changed(object sender, RoutedEventArgs e) => Render(); private static string S(string key) => SetupLocalization.GetString("Onboarding_AiSetup_" + key); private static Visibility Visible(bool value) => value ? Visibility.Visible : Visibility.Collapsed; - private void ShowError(string key) + private void ShowError(string key, string? detail = null) { - if (BackdropLocked) _providerError = S(key); + var message = S(key); + detail ??= key is "VerificationFailed" or "Rejected" + ? Client?.VerificationFailure ?? Client?.Wizard?.Error : null; + if (!string.IsNullOrWhiteSpace(detail)) + { + // Rejected is rendered again when the request settles. Retain the exact verification cause. + var sanitized = SetupLogger.Sanitize(detail); + message += " " + sanitized[..Math.Min(sanitized.Length, 2048)]; + } + if (BackdropLocked) _providerError = message; else { - ErrorBar.Message = S(key); + ErrorBar.Message = message; ErrorBar.IsOpen = true; } Render(); @@ -1238,9 +1249,8 @@ private void ReportFailure(Exception error) ShowError(error is LocalAiSelectionRejectedException ? "LocalChanged" : error is UnauthorizedAccessException ? "AdminRequired" : Client?.Phase == GatewayAiSetupPhase.Prepared ? "PreparedChanged" : - _localExpectedModel is not null || Client?.RequiresReconciliation == true ? "Uncertain" : "Failed"); - if (error is LocalAiSelectionRejectedException) - ErrorBar.Message += " " + error.Message; + _localExpectedModel is not null || Client?.RequiresReconciliation == true ? "Uncertain" : "Failed", + error.Message); Render(); } } diff --git a/src/OpenClaw.SetupEngine/GatewayAiSetupClient.cs b/src/OpenClaw.SetupEngine/GatewayAiSetupClient.cs index b45bc371f5..e0ab6b866f 100644 --- a/src/OpenClaw.SetupEngine/GatewayAiSetupClient.cs +++ b/src/OpenClaw.SetupEngine/GatewayAiSetupClient.cs @@ -32,6 +32,7 @@ public sealed class GatewayAiSetupClient( public GatewayAiSetupWizardResult? Wizard { get; private set; } public string? SessionId { get; private set; } public string? VerifiedModelRef { get; private set; } + public string? VerificationFailure { get; private set; } public bool IsBusy => _busy; public GatewayAiSetupRoute Route => _route; public long ConnectionGeneration => transport.Generation; @@ -167,6 +168,7 @@ private void Select(GatewayAiSetupChoice choice) throw new NotSupportedException("This gateway does not advertise the selected setup method."); Selection = choice; VerifiedModelRef = null; + VerificationFailure = null; Phase = GatewayAiSetupPhase.Choosing; } @@ -344,6 +346,7 @@ private async Task VerifyRouteAsync(string expected, { var generation = transport.Generation; VerifiedModelRef = null; + VerificationFailure = null; _verifiedGeneration = 0; var result = await ExecuteAsync("openclaw.setup.verify", RouteParams(), 120_000, ct); ct.ThrowIfCancellationRequested(); @@ -354,6 +357,7 @@ private async Task VerifyRouteAsync(string expected, throw new InvalidDataException("Verification did not confirm the selected model."); if (!result.Ok && (string.IsNullOrWhiteSpace(result.Status) || string.IsNullOrWhiteSpace(result.Error))) throw new InvalidDataException("The gateway returned an incomplete verification failure."); + VerificationFailure = result.Ok ? null : result.Error; VerifiedModelRef = result.Ok ? result.ModelRef : null; _verifiedGeneration = result.Ok ? generation : 0; return result; diff --git a/tests/OpenClaw.Tray.UITests/OnboardingAiPageTests.cs b/tests/OpenClaw.Tray.UITests/OnboardingAiPageTests.cs index 64a18531e6..6802aee5bc 100644 --- a/tests/OpenClaw.Tray.UITests/OnboardingAiPageTests.cs +++ b/tests/OpenClaw.Tray.UITests/OnboardingAiPageTests.cs @@ -391,7 +391,7 @@ await WithPageAsync(async (page, transport, completed) => Assert.False(Find(page, "CandidateChoices").IsEnabled); Assert.Equal(0, completed()); typeof(AiSetupPage).GetMethod("ShowError", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)! - .Invoke(page, ["Failed"]); + .Invoke(page, ["Failed", null]); var error = Find(page, "ProviderActivityError"); Assert.True(error.IsOpen); Assert.False(string.IsNullOrWhiteSpace(error.Message)); @@ -426,7 +426,7 @@ await WithPageAsync(async (page, transport, completed) => using (await OnboardingNativeProof.CaptureAsync(ui.TestWindow, $"followup-provider-loading-{theme}", output, ["Connect your AI", "Synthetic provider loading", "Cancel"], requiredContent: page)) { } typeof(AiSetupPage).GetMethod("ShowError", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)! - .Invoke(page, ["Failed"]); + .Invoke(page, ["Failed", null]); Assert.True(Find(page, "ProviderActivityError").IsOpen); using (await OnboardingNativeProof.CaptureAsync(ui.TestWindow, $"followup-provider-inline-error-{theme}", output, ["Connect your AI", "Cancel"], requiredContent: page)) { }