Skip to content
Open
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
51 changes: 48 additions & 3 deletions src/OpenClaw.SetupEngine.UI/Pages/WizardPage.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Navigation;
using OpenClaw.Connection;
using OpenClaw.SetupEngine;
using OpenClaw.Shared;
using OpenClaw.SetupEngine.UI;
using Windows.ApplicationModel.DataTransfer;
Expand Down Expand Up @@ -33,6 +34,10 @@ public sealed partial class WizardPage : Page
private int _wizardStepCount;
private int _progressPolls;
private int _totalProgressPolls;
private int _stepIndex;
private int _totalSteps;
private bool _stepHasOptions;
private readonly WizardFinalStepTracker _finalStepTracker = new WizardFinalStepTracker();
private readonly Dictionary<string, int> _stepVisits = new(StringComparer.OrdinalIgnoreCase);
private readonly List<WizardOptionValue> _options = [];
private volatile bool _expectedTerminalRestart;
Expand Down Expand Up @@ -126,6 +131,7 @@ private async Task StartWizardAsync(bool clearTranscript = true)
_totalProgressPolls = 0;
_lastProgressStepId = "";
_stepVisits.Clear();
_finalStepTracker.ResetForNewSession();
SetBusy("Connecting to gateway...");
var client = await ConnectClientAsync();
if (generation != _operationGeneration)
Expand Down Expand Up @@ -284,10 +290,17 @@ private async Task ApplyPayloadAsync(JsonElement payload)
if (payload.TryGetProperty("done", out var done) && done.ValueKind == JsonValueKind.True)
{
var error = payload.TryGetProperty("error", out var err) ? err.ToString() : "";
if (!string.IsNullOrWhiteSpace(error) && !error.Contains("this.prompt is not a function", StringComparison.OrdinalIgnoreCase))
if (!string.IsNullOrWhiteSpace(error))
{
ShowError(error);
return;
var decision = SetupWizardRunner.DecideTerminalWizardError(
payloadIsTerminal: true,
error,
_finalStepTracker.AnsweredFinalStep);
if (!decision.MarksWizardCompleted)
{
ShowError(error);
return;
}
}

await DisconnectAsync();
Expand All @@ -308,11 +321,14 @@ private async Task ApplyPayloadAsync(JsonElement payload)
var rawType = step.TryGetProperty("type", out var type) ? type.ToString() : "note";
_stepType = string.IsNullOrWhiteSpace(rawType) ? "note" : rawType.Trim().ToLowerInvariant();
var stepIndex = payload.TryGetProperty("stepIndex", out var indexProperty) && indexProperty.TryGetInt32(out var index) ? index : 0;
_stepIndex = stepIndex;
_totalSteps = payload.TryGetProperty("totalSteps", out var totalProperty) && totalProperty.TryGetInt32(out var total) ? total : 0;
_sensitive = step.TryGetProperty("sensitive", out var sensitive) && sensitive.ValueKind == JsonValueKind.True;
var title = step.TryGetProperty("title", out var titleProp) ? titleProp.ToString() : "";
var message = WizardPayloadHelpers.ExtractStepMessage(step);
var initial = step.TryGetProperty("initialValue", out var initialProp) ? initialProp : default;
var hasOptions = StepHasOptions(step);
_stepHasOptions = hasOptions;
_stepCategory = WizardStepClassifier.Categorize(_stepType, hasOptions);

if (_stepCategory == WizardStepCategory.RequiresAnswer
Expand Down Expand Up @@ -359,6 +375,7 @@ private async Task ApplyPayloadAsync(JsonElement payload)
if (generation != _operationGeneration || _errorState || _client == null)
return;

_finalStepTracker.RecordProgressAcknowledgement();
payload = await _client.SendWizardRequestAsync(
"wizard.next",
WizardNextPayload.Acknowledge(_sessionId, _stepId),
Expand Down Expand Up @@ -703,6 +720,13 @@ private async Task SendOptionValueAsync(string value)
SetBusy("Loading...");
ClearConsoleBanner();
var parameters = new { sessionId = _sessionId, answer = new { stepId = _stepId, value } };
_finalStepTracker.RecordAnsweredStep(
_stepType,
_stepId,
_currentTitle,
_stepHasOptions,
_stepIndex,
_totalSteps);
var payload = await _client.SendWizardRequestAsync("wizard.next", parameters, timeoutMs: TimeoutForCurrentStep());
if (generation != _operationGeneration) return;
await ApplyPayloadAsync(payload);
Expand All @@ -723,6 +747,13 @@ private async Task ExpandMoreOptionsAsync(string moreValue, List<WizardOptionVal
try
{
var parameters = new { sessionId = _sessionId, answer = new { stepId = _stepId, value = moreValue } };
_finalStepTracker.RecordAnsweredStep(
_stepType,
_stepId,
_currentTitle,
_stepHasOptions,
_stepIndex,
_totalSteps);
var payload = await _client.SendWizardRequestAsync("wizard.next", parameters, timeoutMs: TimeoutForCurrentStep());
if (generation != _operationGeneration) return;

Expand Down Expand Up @@ -839,6 +870,20 @@ private async Task SendCurrentAnswerAsync(bool skip)
parameters = new { sessionId = _sessionId, answer = new { stepId = _stepId, value = answerValue } };
}

if (skip)
{
_finalStepTracker.RecordProgressAcknowledgement();
}
else
{
_finalStepTracker.RecordAnsweredStep(
_stepType,
_stepId,
_currentTitle,
_stepHasOptions,
_stepIndex,
_totalSteps);
}
_expectedTerminalRestart =
!skip &&
_hostAccessPlan.CanControlWslGateway &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,46 @@ public void BestEffortCancel_StaysGatedOnWizardCompleted()
StringComparison.Ordinal);
}

[Fact]
public void WinUiDonePayload_UsesDecideTerminalWizardError()
{
var source = WizardPageSource();
var apply = ExtractMethod(source, "ApplyPayloadAsync");
var start = ExtractMethod(source, "StartWizardAsync");
var sendAnswer = ExtractMethod(source, "SendCurrentAnswerAsync");
var sendOption = ExtractMethod(source, "SendOptionValueAsync");
var expandMore = ExtractMethod(source, "ExpandMoreOptionsAsync");

Assert.Contains("new WizardFinalStepTracker()", source, StringComparison.Ordinal);
Assert.Contains(
"SetupWizardRunner.DecideTerminalWizardError(",
apply,
StringComparison.Ordinal);
Assert.Contains("_finalStepTracker.AnsweredFinalStep", apply, StringComparison.Ordinal);
Assert.Contains("if (!decision.MarksWizardCompleted)", apply, StringComparison.Ordinal);
AssertInOrder(
apply,
"if (!decision.MarksWizardCompleted)",
"ShowError(error);",
"return;");
Assert.DoesNotContain("this.prompt is not a function", apply, StringComparison.Ordinal);
AssertInOrder(
apply,
"_finalStepTracker.RecordProgressAcknowledgement();",
"SendWizardRequestAsync(");
Assert.Contains("_finalStepTracker.ResetForNewSession();", start, StringComparison.Ordinal);
AssertRecordsAnswerBeforeNext(sendAnswer);
AssertInOrder(
sendAnswer,
"if (skip)",
"_finalStepTracker.RecordProgressAcknowledgement();",
"else",
"_finalStepTracker.RecordAnsweredStep(",
"SendWizardRequestAsync(");
AssertRecordsAnswerBeforeNext(sendOption);
AssertRecordsAnswerBeforeNext(expandMore);
}

[Fact]
public void FinalStepTracking_IsOwnedByTheTrackerSeam()
{
Expand All @@ -56,6 +96,57 @@ public void FinalStepTracking_IsOwnedByTheTrackerSeam()
StringComparison.Ordinal);
}

private static void AssertRecordsAnswerBeforeNext(string method)
{
AssertInOrder(
method,
"_finalStepTracker.RecordAnsweredStep(",
"SendWizardRequestAsync(");
}

private static void AssertInOrder(string source, params string[] markers)
{
var current = -1;
foreach (var marker in markers)
{
var next = source.IndexOf(marker, current + 1, StringComparison.Ordinal);
Assert.True(next >= 0, $"Could not find marker after index {current}: {marker}");
current = next;
}
}

private static string ExtractMethod(string source, string methodName)
{
var signature = source.IndexOf($"async Task {methodName}(", StringComparison.Ordinal);
Assert.True(signature >= 0, $"Could not find method {methodName}.");
var brace = source.IndexOf('{', signature);
Assert.True(brace >= 0, $"Could not find body for method {methodName}.");

var depth = 0;
for (var index = brace; index < source.Length; index++)
{
if (source[index] == '{')
depth++;
else if (source[index] == '}')
{
depth--;
if (depth == 0)
return source.Substring(signature, index - signature + 1);
}
}

throw new InvalidOperationException($"Could not extract method {methodName}.");
}

private static string WizardPageSource() =>
File.ReadAllText(
Path.Combine(
RepositoryRoot(),
"src",
"OpenClaw.SetupEngine.UI",
"Pages",
"WizardPage.xaml.cs"));

private static int CountOccurrences(string source, string value)
{
var count = 0;
Expand Down
Loading