Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 18 additions & 3 deletions src/OpenClaw.SetupEngine.UI/Pages/WizardPage.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,20 @@ private async Task ApplyPayloadAsync(JsonElement payload)
return;
}

var validationError = WizardPayloadHelpers.ExtractCurrentStepValidationError(payload, _stepId);
if (!string.IsNullOrWhiteSpace(validationError))
{
BusyRing.Visibility = Visibility.Collapsed;
BusyRing.IsActive = false;
StatusText.Text = "A few quick questions to connect your agent";
SecondaryButton.IsEnabled = true;
ShowRecoveryActions();
UpdateContinueState();
ErrorText.Text = validationError;
ErrorText.Visibility = Visibility.Visible;
return;
}

if (!payload.TryGetProperty("step", out var step))
{
ShowError("Gateway wizard returned an invalid response.");
Expand Down Expand Up @@ -851,7 +865,8 @@ private async Task SendCurrentAnswerAsync(bool skip)
if (generation != _operationGeneration)
return;

AppendTranscriptTurn(answeredQuestion, answeredLabel);
if (string.IsNullOrWhiteSpace(WizardPayloadHelpers.ExtractCurrentStepValidationError(payload, _stepId)))
AppendTranscriptTurn(answeredQuestion, answeredLabel);
await ApplyPayloadAsync(payload);
ScrollActiveIntoView();
}
Expand Down Expand Up @@ -1056,7 +1071,7 @@ private bool TryBuildAnswerValue(out object value)
return true;

if (_stepType == "text")
return !WizardSelection.ShouldDisableContinue(_stepType, value?.ToString());
return WizardSelection.CanSubmitTextAnswer(_stepType);

return !WizardSelection.ShouldDisableContinue(_stepType, GetSelectedOptionValues(), _options.Select(o => o.Value).ToArray());
}
Expand All @@ -1083,7 +1098,7 @@ private void UpdateContinueState()
return;

PrimaryButton.IsEnabled = _stepType == "text"
? !WizardSelection.ShouldDisableContinue(_stepType, _sensitive ? SecretInput.Password : TextInput.Text)
? WizardSelection.CanSubmitTextAnswer(_stepType)
: !WizardSelection.ShouldDisableContinue(
_stepType,
GetSelectedOptionValues(),
Expand Down
23 changes: 23 additions & 0 deletions src/OpenClaw.SetupEngine.UI/WizardPayloadHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,29 @@ namespace OpenClaw.SetupEngine.UI;
/// </summary>
internal static class WizardPayloadHelpers
{
/// <summary>
/// Returns a validation error only when the gateway kept the client on the
/// current step. This distinguishes rejected answers from terminal errors.
/// </summary>
public static string ExtractCurrentStepValidationError(JsonElement payload, string currentStepId)
{
if (payload.ValueKind != JsonValueKind.Object
|| string.IsNullOrWhiteSpace(currentStepId)
|| !payload.TryGetProperty("error", out var error)
|| error.ValueKind != JsonValueKind.String
|| string.IsNullOrWhiteSpace(error.GetString())
|| !payload.TryGetProperty("step", out var step)
|| step.ValueKind != JsonValueKind.Object
|| !step.TryGetProperty("id", out var id)
|| id.ValueKind != JsonValueKind.String
|| !string.Equals(id.GetString(), currentStepId, StringComparison.Ordinal))
{
return string.Empty;
}

return error.GetString()!;
}

/// <summary>
/// Reads the <c>message</c> field of a wizard step. Upstream is supposed to
/// send a string; the Gemini CLI OAuth plugin (and possibly others) nests a
Expand Down
7 changes: 5 additions & 2 deletions src/OpenClaw.SetupEngine/WizardSelection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ public static bool HasValidSelection(string stepType, IReadOnlyCollection<string
public static bool ShouldDisableContinue(string stepType, IReadOnlyCollection<string> selectedValues, IReadOnlyCollection<string> optionValues) =>
RequiresSelection(stepType) && !HasValidSelection(stepType, selectedValues, optionValues);

public static bool ShouldDisableContinue(string stepType, string? textInput) =>
stepType == "text" && string.IsNullOrWhiteSpace(textInput);
// Text validation belongs to the gateway because the wire protocol does not
// expose whether a text step is required. Always submit the current value so
// optional empty answers can advance and required fields can return their
// authoritative validation message.
public static bool CanSubmitTextAnswer(string stepType) => stepType == "text";
}
11 changes: 5 additions & 6 deletions tests/OpenClaw.SetupEngine.Tests/WizardSelectionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,11 @@ public void EmptyAcknowledgeSteps_AllowContinue(string stepType)
}

[Theory]
[InlineData(null, true)]
[InlineData("", true)]
[InlineData(" ", true)]
[InlineData("value", false)]
public void ContinueDisabled_ForEmptyTextInput(string? input, bool expectedDisabled)
[InlineData("text", true)]
[InlineData("select", false)]
[InlineData("note", false)]
public void TextAnswers_AreSubmittedForGatewayValidation(string stepType, bool expected)
{
Assert.Equal(expectedDisabled, WizardSelection.ShouldDisableContinue("text", input));
Assert.Equal(expected, WizardSelection.CanSubmitTextAnswer(stepType));
}
}
15 changes: 15 additions & 0 deletions tests/OpenClaw.Tray.Tests/AppRefactorContractTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1479,6 +1479,21 @@ public void WizardProgressPolling_UsesStepIdForTimeoutClassification()
Assert.Contains("WizardTimeouts.ForStep(title, message, _stepId)", source);
}

[Fact]
public void WizardValidationError_RemainsVisibleAfterContinueStateRefresh()
{
var root = TestRepositoryPaths.GetRepositoryRoot();
var source = File.ReadAllText(Path.Combine(root, "src", "OpenClaw.SetupEngine.UI", "Pages", "WizardPage.xaml.cs"));
var applyPayload = ExtractMethod(source, "ApplyPayloadAsync");

AssertInOrder(
applyPayload,
"ExtractCurrentStepValidationError",
"UpdateContinueState();",
"ErrorText.Text = validationError;",
"ErrorText.Visibility = Visibility.Visible;");
}

[Fact]
public void WizardResetInputs_RemovesOverflowMoreButton()
{
Expand Down
22 changes: 22 additions & 0 deletions tests/OpenClaw.Tray.Tests/WizardPayloadHelpersTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,28 @@ public class WizardPayloadHelpersTests
private static JsonElement Parse(string json)
=> JsonDocument.Parse(json).RootElement;

[Fact]
public void ExtractCurrentStepValidationError_returns_error_for_rejected_answer()
{
var payload = Parse("""{"done":false,"error":"Model ID is required","step":{"id":"model","type":"text"}}""");

Assert.Equal(
"Model ID is required",
WizardPayloadHelpers.ExtractCurrentStepValidationError(payload, "model"));
}

[Theory]
[InlineData("other")]
[InlineData("")]
public void ExtractCurrentStepValidationError_ignores_non_current_steps(string currentStepId)
{
var payload = Parse("""{"done":false,"error":"Model ID is required","step":{"id":"model","type":"text"}}""");

Assert.Equal(
string.Empty,
WizardPayloadHelpers.ExtractCurrentStepValidationError(payload, currentStepId));
}

// ---- ExtractStepMessage -----------------------------------------------

[Fact]
Expand Down