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
2 changes: 2 additions & 0 deletions docs/ONBOARDING_WIZARD.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ If the gateway doesn't support the wizard protocol or is unreachable, this scree

The wizard keeps recovery choices visible while setup steps are running so users can start the wizard again or skip it for now if an auth flow stalls. If the gateway restarts or the wizard connection is lost while setup is running, the same recovery choices are presented in the error state so the user is not trapped retrying a broken session.

Gateway-driven onboarding does not show a Back action. The Gateway wizard protocol has no rewind operation, so replaying an earlier Companion payload would display stale state while the authoritative Gateway session remained on a later step. Users can restart onboard or skip and exit from **More options** instead.

Exact Gateway 2026.7.1 has a terminal compatibility path for an app-managed local WSL gateway. When the final `model-check` answer produces WebSocket close 1012 before the gateway can return `done`, setup retries the temporary `NoListener` state and the typed snapshot-changed race that can occur while the listener is restarting. Other unknown or conflicting endpoint ownership fails immediately, and no credential is sent until the managed endpoint is verified again. A retryable startup close 1013 remains inside the existing reconnect timeout. Setup completes only after a fresh authenticated `hello-ok` handshake. Other versions and steps keep the normal managed-local wizard replay behavior with the same bounded ownership wait; remote gateways and other disconnects do not enter this recovery path.

The headless setup engine also treats one terminal wizard payload as completion instead of failure. When the answers applied by the wizard restart the gateway, the gateway can tear down its own hosted wizard TUI and return a terminal payload whose error is exactly `Error: TUI exited from signal SIGTERM`. Setup accepts that result only when the payload is terminal and the request it just sent answered the authoritative final step, so the wizard is not cancelled after it already finished. The final step must be a plain acknowledgement note with no options whose id or title normalizes to `done`, and when the gateway supplies step position metadata it must also be the last step. An earlier `SIGTERM`, a progress poll, a replayed wizard session, any answerable step, any other step id or title, a non-terminal payload, and any other message (different signal, extra text, or different casing) all keep the wizard failure. Only surrounding whitespace is tolerated in the message. Reload-mode restoration, the one-shot managed restart, health verification, and provenance checks are unchanged and still fail closed.
Expand Down
3 changes: 0 additions & 3 deletions src/OpenClaw.SetupEngine.UI/Pages/WizardPage.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,6 @@
<Border Width="8" Height="8" CornerRadius="4" VerticalAlignment="Center" Background="{ThemeResource SetupInactiveDotBrush}" />
</StackPanel>
<StackPanel Grid.Column="0" Orientation="Horizontal" HorizontalAlignment="Left" Spacing="8">
<Button x:Name="WizardBackButton" Content="Back"
MinWidth="100"
Click="WizardBack_Click" />
<DropDownButton x:Name="MoreOptionsButton"
Content="More options"
MinWidth="100"
Expand Down
27 changes: 2 additions & 25 deletions src/OpenClaw.SetupEngine.UI/Pages/WizardPage.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ public sealed partial class WizardPage : Page
private int _totalProgressPolls;
private readonly Dictionary<string, int> _stepVisits = new(StringComparer.OrdinalIgnoreCase);
private readonly List<WizardOptionValue> _options = [];
private readonly Stack<JsonElement> _stepHistory = new();
private volatile bool _expectedTerminalRestart;
// "More ▾" overflow toggle lives as a sibling of SelectOptions, so track it to remove between steps.
private Button? _moreOptionsButton;
Expand Down Expand Up @@ -378,16 +377,13 @@ private async Task ApplyPayloadAsync(JsonElement payload)
}

ResetInputs();
// Push current payload so Back can re-render this step
_stepHistory.Push(payload);
TitleText.Text = string.IsNullOrWhiteSpace(title) ? DisplayTitleFor(_stepType) : title;
RenderMessage(message);
StepCard.MinHeight = _stepType == "note" && string.IsNullOrWhiteSpace(message) ? 140 : 260;
ErrorText.Visibility = Visibility.Collapsed;
BusyRing.Visibility = Visibility.Collapsed;
BusyRing.IsActive = false;
ShowRecoveryActions();
WizardBackButton.Visibility = Visibility.Visible;
StatusText.Text = "A few quick questions to connect your agent";
PrimaryButton.IsEnabled = !WizardSelection.RequiresAnswer(_stepType);
SecondaryButton.IsEnabled = true;
Expand Down Expand Up @@ -443,7 +439,6 @@ private void RenderProgressStep(string title, string message)
PrimaryButton.Content = "Continue";
SecondaryButton.IsEnabled = false;
SecondaryButton.Visibility = Visibility.Collapsed;
WizardBackButton.Visibility = Visibility.Collapsed;
ShowRecoveryActions();
}

Expand All @@ -465,7 +460,8 @@ private bool BuildOptions(JsonElement step, JsonElement initial)
{
SelectOptions.Visibility = Visibility.Visible;

// Reorder: skip options first, then non-more options, filter out "more" and "back" options
// The Gateway contract has no rewind operation. Do not expose back options
// as local navigation because that would desynchronize the active session.
var skipOptions = _options.Where(IsSkipOption).ToList();
var moreOptions = _options.Where(IsMoreOption).ToList();
var normalOptions = _options.Where(o => !IsSkipOption(o) && !IsMoreOption(o) && !IsBackOption(o)).ToList();
Expand Down Expand Up @@ -754,9 +750,6 @@ private async Task ExpandMoreOptionsAsync(string moreValue, List<WizardOptionVal
// Select first item by default
if (SelectOptions.Items.Count > 0)
SelectOptions.SelectedIndex = 0;

// Push this expanded payload to step history so Back works
_stepHistory.Push(payload);
}
}
catch (Exception ex)
Expand All @@ -766,21 +759,6 @@ private async Task ExpandMoreOptionsAsync(string moreValue, List<WizardOptionVal
}
}

private void WizardBack_Click(object sender, RoutedEventArgs e)
{
// Pop the current step (that's showing now), then re-render the previous one
if (_stepHistory.Count > 1)
{
_stepHistory.Pop(); // discard current
var previousPayload = _stepHistory.Pop(); // will be re-pushed by ApplyPayloadAsync
_ = ApplyPayloadAsync(previousPayload);
}
else
{
SetupWindow.Active?.NavigateToWelcome(back: true);
}
}

private void StartOver_Click(object sender, RoutedEventArgs e) =>
AsyncEventHandlerGuard.Run(
StartOverAsync,
Expand All @@ -790,7 +768,6 @@ private void StartOver_Click(object sender, RoutedEventArgs e) =>
private async Task StartOverAsync()
{
AdvanceOperationGeneration();
_stepHistory.Clear();
HideRecoveryActions();
SetBusy("Starting over...");
await CancelCurrentSessionAsync();
Expand Down
16 changes: 16 additions & 0 deletions tests/OpenClaw.Tray.Tests/AppRefactorContractTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1419,6 +1419,22 @@ public void WizardResetInputs_RemovesOverflowMoreButton()
Assert.Contains("_moreOptionsButton = null", reset);
}

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

Assert.DoesNotContain("WizardBackButton", xaml);
Assert.DoesNotContain("WizardBack_Click", source);
Assert.DoesNotContain("_stepHistory", source);
Assert.DoesNotContain("ApplyPayloadAsync(previousPayload)", source);
Assert.Contains("MoreOptionsButton", xaml);
Assert.Contains("StartOver_Click", xaml);
Assert.Contains("SkipWizard_Click", xaml);
}

[Fact]
public void WizardCompletion_AppliesWindowsNodeContextBeforeSummary()
{
Expand Down
Loading