diff --git a/docs/setup-wizard-ux/01-welcome-security.png b/docs/setup-wizard-ux/01-welcome-security.png
new file mode 100644
index 000000000..c0cbf11b7
Binary files /dev/null and b/docs/setup-wizard-ux/01-welcome-security.png differ
diff --git a/docs/setup-wizard-ux/02-setup-mode-listview.png b/docs/setup-wizard-ux/02-setup-mode-listview.png
new file mode 100644
index 000000000..5baeab004
Binary files /dev/null and b/docs/setup-wizard-ux/02-setup-mode-listview.png differ
diff --git a/docs/setup-wizard-ux/03-config-handling.png b/docs/setup-wizard-ux/03-config-handling.png
new file mode 100644
index 000000000..ea35d1689
Binary files /dev/null and b/docs/setup-wizard-ux/03-config-handling.png differ
diff --git a/docs/setup-wizard-ux/04-provider-collapsed.png b/docs/setup-wizard-ux/04-provider-collapsed.png
new file mode 100644
index 000000000..f2737404e
Binary files /dev/null and b/docs/setup-wizard-ux/04-provider-collapsed.png differ
diff --git a/docs/setup-wizard-ux/05-provider-expanded.png b/docs/setup-wizard-ux/05-provider-expanded.png
new file mode 100644
index 000000000..4ff72ee78
Binary files /dev/null and b/docs/setup-wizard-ux/05-provider-expanded.png differ
diff --git a/docs/setup-wizard-ux/06-auth-method.png b/docs/setup-wizard-ux/06-auth-method.png
new file mode 100644
index 000000000..d8728a3c3
Binary files /dev/null and b/docs/setup-wizard-ux/06-auth-method.png differ
diff --git a/docs/setup-wizard-ux/07-text-input.png b/docs/setup-wizard-ux/07-text-input.png
new file mode 100644
index 000000000..956ffe96b
Binary files /dev/null and b/docs/setup-wizard-ux/07-text-input.png differ
diff --git a/docs/setup-wizard-ux/08-capabilities-a11y-fixed.png b/docs/setup-wizard-ux/08-capabilities-a11y-fixed.png
new file mode 100644
index 000000000..100ba4532
Binary files /dev/null and b/docs/setup-wizard-ux/08-capabilities-a11y-fixed.png differ
diff --git a/docs/setup-wizard-ux/README.md b/docs/setup-wizard-ux/README.md
new file mode 100644
index 000000000..a9c93c22d
--- /dev/null
+++ b/docs/setup-wizard-ux/README.md
@@ -0,0 +1,47 @@
+# Setup wizard UX refresh — screens & accessibility proof
+
+Screenshots captured from the live isolated dev app (`OPENCLAW_FORCE_ONBOARDING=1`,
+`run-app-local.ps1 -NoBuild -Isolated`) walking the full gateway setup + onboard flow.
+Accessibility was scanned per-screen with a standalone Axe.Windows (v2.4.1) scanner
+against the live process — the same engine Accessibility Insights uses.
+
+## Screens
+
+| # | Screen | Notes |
+|---|--------|-------|
+| 01 | Welcome + security notice | Selectable RichTextBlock body |
+| 02 | Setup mode | Single-selection `ListView` (ItemContainer), first item selected by default |
+| 03 | Config handling | Single-selection list, standardized bottom bar |
+| 04 | Model/auth provider (collapsed) | "Skip for now" pinned to top, subtle **More ▾** expander |
+| 05 | Model/auth provider (expanded) | One-click expand keeps "Skip for now"; full provider list |
+| 06 | Auth method | Single-selection list |
+| 07 | Text input step | API-key entry |
+| 08 | Capabilities | Radio group with accessible names (a11y fix) |
+
+Bottom action bar across steps: standard **Back** button, `AccentButtonStyle`
+primary (Continue), subtle **More options** / **Skip** / **Start over** controls.
+
+## Accessibility results (Axe.Windows, actionable violations)
+
+All refactored surfaces flagged `NameNotNull` (focusable element with a null
+accessible Name) before the fix; every one is 0 after.
+
+| Screen | Before | After |
+|--------|:------:|:-----:|
+| Capabilities radio group | 3 | 0 |
+| Setup mode ListView | 2 | 0 |
+| Provider list (collapsed) | 6 | 0 |
+| Provider list (expanded) | 7 | 0 |
+| Welcome / Confirm / Auth / Text input | 0 | 0 |
+
+### Fixes
+- `WizardPage` option lists: each `ListViewItem` (and multiselect `CheckBox`) now
+ gets `AutomationProperties.Name` = label + hint via a shared `CreateOptionItem`
+ helper.
+- `CapabilitiesPage`: the three profile radio buttons (Read-only / Standard /
+ Full access) now set `AutomationProperties.Name`.
+
+### Known framework limitation (not fixed)
+Inline `Hyperlink` elements inside a `RichTextBlock` (security disclaimer link)
+report a null `BoundingRectangle` to UIA when scrolled out of view. This is a
+WinUI framework behavior for inline hyperlinks, not a control we construct.
diff --git a/src/OpenClaw.SetupEngine.UI/Pages/AdvancedSetupPage.xaml b/src/OpenClaw.SetupEngine.UI/Pages/AdvancedSetupPage.xaml
index 3622b58b2..326f11d39 100644
--- a/src/OpenClaw.SetupEngine.UI/Pages/AdvancedSetupPage.xaml
+++ b/src/OpenClaw.SetupEngine.UI/Pages/AdvancedSetupPage.xaml
@@ -86,10 +86,10 @@
diff --git a/src/OpenClaw.SetupEngine.UI/Pages/CapabilitiesPage.xaml b/src/OpenClaw.SetupEngine.UI/Pages/CapabilitiesPage.xaml
index 79350ed36..d8d8f6cac 100644
--- a/src/OpenClaw.SetupEngine.UI/Pages/CapabilitiesPage.xaml
+++ b/src/OpenClaw.SetupEngine.UI/Pages/CapabilitiesPage.xaml
@@ -39,7 +39,7 @@
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="OpenClaw runs your agent in a private, self-hosted gateway on this PC. Pick what it's allowed to do — you can change this later in Companion Settings." />
-
+
-
+
@@ -63,7 +63,7 @@
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
-
+
diff --git a/src/OpenClaw.SetupEngine.UI/Pages/CompletePage.xaml b/src/OpenClaw.SetupEngine.UI/Pages/CompletePage.xaml
index 4c61aed47..1f692405c 100644
--- a/src/OpenClaw.SetupEngine.UI/Pages/CompletePage.xaml
+++ b/src/OpenClaw.SetupEngine.UI/Pages/CompletePage.xaml
@@ -138,7 +138,7 @@
diff --git a/src/OpenClaw.SetupEngine.UI/Pages/ProgressPage.xaml b/src/OpenClaw.SetupEngine.UI/Pages/ProgressPage.xaml
index 787e9a4d6..2526d26aa 100644
--- a/src/OpenClaw.SetupEngine.UI/Pages/ProgressPage.xaml
+++ b/src/OpenClaw.SetupEngine.UI/Pages/ProgressPage.xaml
@@ -114,7 +114,7 @@
diff --git a/src/OpenClaw.SetupEngine.UI/Pages/SecurityNoticePage.xaml b/src/OpenClaw.SetupEngine.UI/Pages/SecurityNoticePage.xaml
index 24ad88aa1..7d932338d 100644
--- a/src/OpenClaw.SetupEngine.UI/Pages/SecurityNoticePage.xaml
+++ b/src/OpenClaw.SetupEngine.UI/Pages/SecurityNoticePage.xaml
@@ -38,7 +38,7 @@
diff --git a/src/OpenClaw.SetupEngine.UI/Pages/WelcomePage.xaml b/src/OpenClaw.SetupEngine.UI/Pages/WelcomePage.xaml
index 25f4340de..a9612f634 100644
--- a/src/OpenClaw.SetupEngine.UI/Pages/WelcomePage.xaml
+++ b/src/OpenClaw.SetupEngine.UI/Pages/WelcomePage.xaml
@@ -25,14 +25,15 @@
-
-
-
+
-
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/OpenClaw.SetupEngine.UI/Pages/WelcomePage.xaml.cs b/src/OpenClaw.SetupEngine.UI/Pages/WelcomePage.xaml.cs
index d414071e7..3e01f07b5 100644
--- a/src/OpenClaw.SetupEngine.UI/Pages/WelcomePage.xaml.cs
+++ b/src/OpenClaw.SetupEngine.UI/Pages/WelcomePage.xaml.cs
@@ -2,6 +2,8 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Hosting;
+using Microsoft.UI.Xaml.Input;
+using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Navigation;
using OpenClaw.SetupEngine;
using OpenClaw.SetupEngine.UI;
@@ -15,11 +17,13 @@ public sealed partial class WelcomePage : Page
private const string InstallButtonText = "Install a local gateway (WSL)";
private const string CheckingButtonText = "Checking existing setup...";
private SetupConfig? _config;
+ private bool _installSelected = true; // default selection
public WelcomePage()
{
InitializeComponent();
Loaded += OnLoaded;
+ UpdateCardSelection();
}
protected override void OnNavigatedTo(NavigationEventArgs e)
@@ -50,19 +54,58 @@ private void StartMascotBreatheAnimation()
visual.StartAnimation("Scale", pulse);
}
- private void StartButton_Click(object sender, RoutedEventArgs e) =>
- AsyncEventHandlerGuard.Run(
- StartButtonClickAsync,
- NullLogger.Instance,
- nameof(StartButton_Click));
+ private void InstallCard_Pressed(object sender, PointerRoutedEventArgs e)
+ {
+ _installSelected = true;
+ UpdateCardSelection();
+ }
+
+ private void ConnectCard_Pressed(object sender, PointerRoutedEventArgs e)
+ {
+ _installSelected = false;
+ UpdateCardSelection();
+ }
+
+ private void UpdateCardSelection()
+ {
+ InstallCard.BorderBrush = _installSelected
+ ? (Brush)Application.Current.Resources["AccentFillColorDefaultBrush"]
+ : (Brush)Application.Current.Resources["CardStrokeColorDefaultBrush"];
+ InstallCard.BorderThickness = new Thickness(_installSelected ? 2 : 1);
+
+ ConnectCard.BorderBrush = !_installSelected
+ ? (Brush)Application.Current.Resources["AccentFillColorDefaultBrush"]
+ : (Brush)Application.Current.Resources["CardStrokeColorDefaultBrush"];
+ ConnectCard.BorderThickness = new Thickness(!_installSelected ? 2 : 1);
+ }
+
+ private void Back_Click(object sender, RoutedEventArgs e)
+ {
+ SetupWindow.Active?.NavigateToSecurityNotice(back: true);
+ }
- private async Task StartButtonClickAsync()
+ private void Next_Click(object sender, RoutedEventArgs e)
+ {
+ if (_installSelected)
+ {
+ AsyncEventHandlerGuard.Run(
+ StartInstallAsync,
+ NullLogger.Instance,
+ nameof(Next_Click));
+ }
+ else
+ {
+ SetupWindow.Active?.NavigateToAdvancedSetup();
+ }
+ }
+
+ private async Task StartInstallAsync()
{
var config = _config ?? throw new InvalidOperationException("Setup configuration has not been loaded.");
var setupWindow = SetupWindow.Active;
var dataDir = setupWindow?.DataDir ?? SetupContext.ResolveDataDir();
- InstallButton.IsEnabled = false;
+ NextButton.IsEnabled = false;
InstallTitle.Text = CheckingButtonText;
var navigating = false;
try
@@ -98,14 +141,8 @@ private async Task StartButtonClickAsync()
if (!navigating && setupWindow is { IsClosed: false })
{
InstallTitle.Text = InstallButtonText;
- InstallButton.IsEnabled = true;
+ NextButton.IsEnabled = true;
}
}
}
-
- private void AdvancedSetup_Click(object sender, RoutedEventArgs e)
- {
- // Show quick connect instructions before handing off to the companion app.
- SetupWindow.Active?.NavigateToAdvancedSetup();
- }
}
diff --git a/src/OpenClaw.SetupEngine.UI/Pages/WizardPage.xaml b/src/OpenClaw.SetupEngine.UI/Pages/WizardPage.xaml
index 93c64bb67..cdc552750 100644
--- a/src/OpenClaw.SetupEngine.UI/Pages/WizardPage.xaml
+++ b/src/OpenClaw.SetupEngine.UI/Pages/WizardPage.xaml
@@ -29,9 +29,11 @@
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}">
-
-
+
+
+
-
+
@@ -61,15 +67,14 @@
Spacing="12">
+
@@ -86,37 +91,42 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
diff --git a/src/OpenClaw.SetupEngine.UI/Pages/WizardPage.xaml.cs b/src/OpenClaw.SetupEngine.UI/Pages/WizardPage.xaml.cs
index 02a6c56d3..faaa16d73 100644
--- a/src/OpenClaw.SetupEngine.UI/Pages/WizardPage.xaml.cs
+++ b/src/OpenClaw.SetupEngine.UI/Pages/WizardPage.xaml.cs
@@ -1,5 +1,6 @@
using System.Text.Json;
using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Automation;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Documents;
using Microsoft.UI.Xaml.Media;
@@ -34,6 +35,9 @@ public sealed partial class WizardPage : Page
private int _totalProgressPolls;
private readonly Dictionary _stepVisits = new(StringComparer.OrdinalIgnoreCase);
private readonly List _options = [];
+ private readonly Stack _stepHistory = new();
+ // "More ▾" overflow toggle lives as a sibling of SelectOptions, so track it to remove between steps.
+ private Button? _moreOptionsButton;
// wizard.payload frames do not include plugin console output, so tail the gateway log inline.
private WizardConsoleTail? _consoleTail;
// Captured on connect for "Open terminal" / "Restart gateway" recovery actions.
@@ -80,15 +84,13 @@ private void RenderWizardPreview()
SelectOptions.Visibility = Visibility.Visible;
foreach (var (val, lbl) in new[] { ("opus", "claude-opus-4.8"), ("sonnet", "claude-sonnet-4.6"), ("haiku", "claude-haiku-4.5") })
{
- SelectOptions.Children.Add(new RadioButton
+ SelectOptions.Items.Add(new ListViewItem
{
Content = lbl,
Tag = val,
- GroupName = "preview",
- Padding = new Thickness(8, 6, 8, 6),
});
}
- ((RadioButton)SelectOptions.Children[0]).IsChecked = true;
+ SelectOptions.SelectedIndex = 0;
PrimaryButton.Content = "Continue";
PrimaryButton.IsEnabled = true;
SecondaryButton.Visibility = Visibility.Collapsed;
@@ -331,6 +333,8 @@ 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;
@@ -338,6 +342,7 @@ private async Task ApplyPayloadAsync(JsonElement payload)
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;
@@ -393,6 +398,7 @@ private void RenderProgressStep(string title, string message)
PrimaryButton.Content = "Continue";
SecondaryButton.IsEnabled = false;
SecondaryButton.Visibility = Visibility.Collapsed;
+ WizardBackButton.Visibility = Visibility.Collapsed;
ShowRecoveryActions();
}
@@ -413,27 +419,88 @@ private bool BuildOptions(JsonElement step, JsonElement initial)
if (_stepType == "select")
{
SelectOptions.Visibility = Visibility.Visible;
- foreach (var option in _options)
+
+ // Reorder: skip options first, then non-more options, filter out "more" and "back" options
+ var skipOptions = _options.Where(IsSkipOption).ToList();
+ var moreOptions = _options.Where(IsMoreOption).ToList();
+ var normalOptions = _options.Where(o => !IsSkipOption(o) && !IsMoreOption(o) && !IsBackOption(o)).ToList();
+
+ var orderedOptions = new List();
+ orderedOptions.AddRange(skipOptions);
+ orderedOptions.AddRange(normalOptions);
+
+ // Show first batch; if there are many options, show a "More" button to expand
+ const int initialVisibleCount = 6;
+ var hasOverflow = orderedOptions.Count > initialVisibleCount;
+ var hasGatewayMore = moreOptions.Count > 0;
+ var showMoreButton = hasOverflow || hasGatewayMore;
+ var visibleOptions = hasOverflow
+ ? orderedOptions.Take(initialVisibleCount).ToList()
+ : orderedOptions;
+
+ foreach (var option in visibleOptions)
{
- SelectOptions.Children.Add(new RadioButton
+ SelectOptions.Items.Add(CreateOptionItem(option));
+ }
+
+ // Add "More ▼" button if there are hidden options OR gateway sent a "more" option
+ if (showMoreButton)
+ {
+ var moreButton = new Button
{
- Content = BuildOptionContent(option),
- Tag = option,
- GroupName = $"wizard-step-{_stepId}",
- Padding = new Thickness(8, 6, 8, 6),
- Margin = new Thickness(0, 0, 0, 2),
- HorizontalAlignment = HorizontalAlignment.Stretch,
- HorizontalContentAlignment = HorizontalAlignment.Stretch
- });
+ Content = "More ▾",
+ MinWidth = 100,
+ HorizontalAlignment = HorizontalAlignment.Center,
+ Background = new Microsoft.UI.Xaml.Media.SolidColorBrush(Microsoft.UI.Colors.Transparent),
+ BorderBrush = new Microsoft.UI.Xaml.Media.SolidColorBrush(Microsoft.UI.Colors.Transparent),
+ Margin = new Thickness(0, 4, 0, 0)
+ };
+ var remainingOptions = hasOverflow ? orderedOptions.Skip(initialVisibleCount).ToList() : new List();
+ var gatewayMoreValue = moreOptions.Count > 0 ? moreOptions[0].Value : null;
+ var capturedSkipOptions = skipOptions;
+ moreButton.Click += (_, _) =>
+ {
+ // Remove the More button
+ if (moreButton.Parent is Panel parent)
+ parent.Children.Remove(moreButton);
+ if (ReferenceEquals(_moreOptionsButton, moreButton))
+ _moreOptionsButton = null;
+
+ if (hasGatewayMore && !string.IsNullOrEmpty(gatewayMoreValue))
+ {
+ // Send the "more" option value to gateway to fetch full list, expanding inline
+ AsyncEventHandlerGuard.Run(
+ async () => await ExpandMoreOptionsAsync(gatewayMoreValue, capturedSkipOptions),
+ NullLogger.Instance,
+ "MoreExpand");
+ }
+ else
+ {
+ // Just expand remaining local options
+ foreach (var option in remainingOptions)
+ {
+ SelectOptions.Items.Add(CreateOptionItem(option));
+ }
+ }
+ };
+ // Insert the button after SelectOptions in the parent StackPanel
+ var parentPanel = SelectOptions.Parent as Panel;
+ if (parentPanel != null)
+ {
+ var idx = parentPanel.Children.IndexOf(SelectOptions);
+ parentPanel.Children.Insert(idx + 1, moreButton);
+ _moreOptionsButton = moreButton;
+ }
}
var initialValue = WizardAnswerBuilder.ValueKeys(initial).FirstOrDefault();
- var index = WizardSelection.SelectedIndex(initialValue, _options.Select(o => o.Value).ToArray());
- if (index >= 0 && index < SelectOptions.Children.Count && SelectOptions.Children[index] is RadioButton radio)
- radio.IsChecked = true;
+ var index = WizardSelection.SelectedIndex(initialValue, visibleOptions.Select(o => o.Value).ToArray());
+ if (index >= 0 && index < SelectOptions.Items.Count)
+ SelectOptions.SelectedIndex = index;
+ else if (SelectOptions.Items.Count > 0)
+ SelectOptions.SelectedIndex = 0;
- foreach (var optionRadio in SelectOptions.Children.OfType())
- optionRadio.Checked += (_, _) => UpdateContinueState();
+ SelectOptions.SelectionChanged += (_, _) => UpdateContinueState();
UpdateContinueState();
}
@@ -455,6 +522,7 @@ private bool BuildOptions(JsonElement step, JsonElement initial)
HorizontalAlignment = HorizontalAlignment.Stretch,
HorizontalContentAlignment = HorizontalAlignment.Stretch
};
+ AutomationProperties.SetName(checkBox, OptionAccessibleName(option));
checkBox.Checked += (_, _) => UpdateContinueState();
checkBox.Unchecked += (_, _) => UpdateContinueState();
MultiOptions.Children.Add(checkBox);
@@ -466,6 +534,24 @@ private bool BuildOptions(JsonElement step, JsonElement initial)
return true;
}
+ private static ListViewItem CreateOptionItem(WizardOptionValue option)
+ {
+ var item = new ListViewItem
+ {
+ Content = BuildOptionContent(option),
+ Tag = option,
+ Padding = new Thickness(12, 10, 12, 10),
+ HorizontalContentAlignment = HorizontalAlignment.Stretch
+ };
+ AutomationProperties.SetName(item, OptionAccessibleName(option));
+ return item;
+ }
+
+ private static string OptionAccessibleName(WizardOptionValue option) =>
+ string.IsNullOrWhiteSpace(option.Hint)
+ ? option.Label
+ : $"{option.Label}. {option.Hint}";
+
private static FrameworkElement BuildOptionContent(WizardOptionValue option)
{
var panel = new StackPanel
@@ -497,6 +583,20 @@ private static FrameworkElement BuildOptionContent(WizardOptionValue option)
return panel;
}
+ private static bool IsSkipOption(WizardOptionValue option) =>
+ string.Equals(option.Value, "__skip__", StringComparison.OrdinalIgnoreCase)
+ || option.Label.Contains("skip", StringComparison.OrdinalIgnoreCase);
+
+ private static bool IsMoreOption(WizardOptionValue option) =>
+ string.Equals(option.Value, "__more__", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(option.Value, "__more", StringComparison.OrdinalIgnoreCase)
+ || (option.Label.Contains("more", StringComparison.OrdinalIgnoreCase)
+ && option.Label.Length < 20);
+
+ private static bool IsBackOption(WizardOptionValue option) =>
+ string.Equals(option.Value, "__back", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(option.Value, "back", StringComparison.OrdinalIgnoreCase);
+
private static Brush ResourceBrush(string key)
{
return Application.Current.Resources.TryGetValue(key, out var brush)
@@ -541,6 +641,101 @@ private async Task SecondaryClickAsync()
await SendCurrentAnswerAsync(skip: true);
}
+ private async Task SendOptionValueAsync(string value)
+ {
+ if (_client == null) return;
+
+ var generation = _operationGeneration;
+ try
+ {
+ SetBusy("Loading...");
+ ClearConsoleBanner();
+ var parameters = new { sessionId = _sessionId, answer = new { stepId = _stepId, value } };
+ var payload = await _client.SendWizardRequestAsync("wizard.next", parameters, timeoutMs: TimeoutForCurrentStep());
+ if (generation != _operationGeneration) return;
+ await ApplyPayloadAsync(payload);
+ ScrollActiveIntoView();
+ }
+ catch (Exception ex)
+ {
+ if (generation != _operationGeneration) return;
+ await EnterWizardErrorAsync(ex.Message);
+ }
+ }
+
+ private async Task ExpandMoreOptionsAsync(string moreValue, List previousSkipOptions)
+ {
+ if (_client == null) return;
+
+ var generation = _operationGeneration;
+ try
+ {
+ var parameters = new { sessionId = _sessionId, answer = new { stepId = _stepId, value = moreValue } };
+ var payload = await _client.SendWizardRequestAsync("wizard.next", parameters, timeoutMs: TimeoutForCurrentStep());
+ if (generation != _operationGeneration) return;
+
+ // Parse the expanded options from the response
+ if (payload.TryGetProperty("step", out var step))
+ {
+ // Update step ID — the gateway may issue a new one for the expanded view
+ if (step.TryGetProperty("id", out var expandedId))
+ _stepId = expandedId.ToString();
+
+ var expandedOptions = WizardAnswerBuilder.ReadOptions(step).ToList();
+
+ // Filter out __back and __more from expanded list
+ var filtered = expandedOptions
+ .Where(o => !IsMoreOption(o) && !IsBackOption(o))
+ .ToList();
+
+ // Rebuild the ListView: skip options first, then all expanded items
+ SelectOptions.Items.Clear();
+ _options.Clear();
+
+ // Re-inject skip options at top
+ foreach (var skip in previousSkipOptions)
+ {
+ _options.Add(skip);
+ SelectOptions.Items.Add(CreateOptionItem(skip));
+ }
+
+ // Add all expanded options
+ foreach (var option in filtered)
+ {
+ _options.Add(option);
+ SelectOptions.Items.Add(CreateOptionItem(option));
+ }
+
+ // 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)
+ {
+ if (generation != _operationGeneration) return;
+ await EnterWizardErrorAsync(ex.Message);
+ }
+ }
+
+ 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,
@@ -550,6 +745,7 @@ private void StartOver_Click(object sender, RoutedEventArgs e) =>
private async Task StartOverAsync()
{
AdvanceOperationGeneration();
+ _stepHistory.Clear();
HideRecoveryActions();
SetBusy("Starting over...");
await CancelCurrentSessionAsync();
@@ -728,9 +924,7 @@ private bool TryBuildAnswerValue(out object value)
value = _stepType switch
{
"confirm" => true,
- "select" => SelectOptions.Children.OfType()
- .FirstOrDefault(r => r.IsChecked == true)
- ?.Tag is WizardOptionValue option
+ "select" => SelectOptions.SelectedItem is ListViewItem { Tag: WizardOptionValue option }
? option.RawValue
: "",
"multiselect" => MultiOptions.Children.OfType()
@@ -756,11 +950,9 @@ private string[] GetSelectedOptionValues()
{
return _stepType switch
{
- "select" => SelectOptions.Children.OfType()
- .Where(r => r.IsChecked == true)
- .Select(r => r.Tag is WizardOptionValue option ? option.Value : "")
- .Where(v => v.Length > 0)
- .ToArray(),
+ "select" => SelectOptions.SelectedItem is ListViewItem { Tag: WizardOptionValue selectedOpt }
+ ? [selectedOpt.Value]
+ : [],
"multiselect" => MultiOptions.Children.OfType()
.Where(c => c.IsChecked == true)
.Select(c => c.Tag is WizardOptionValue option ? option.Value : "")
@@ -802,53 +994,80 @@ private int TimeoutForCurrentStep()
private void ResetInputs()
{
- SelectOptions.Children.Clear();
+ if (_moreOptionsButton?.Parent is Panel morePanel)
+ morePanel.Children.Remove(_moreOptionsButton);
+ _moreOptionsButton = null;
+ SelectOptions.Items.Clear();
SelectOptions.Visibility = Visibility.Collapsed;
MultiOptions.Children.Clear();
MultiOptions.Visibility = Visibility.Collapsed;
TextInput.Visibility = Visibility.Collapsed;
SecretInput.Visibility = Visibility.Collapsed;
- MessagePanel.Children.Clear();
+ MessageBlock.Blocks.Clear();
+ MessageBlock.Visibility = Visibility.Collapsed;
+ MessageCodeRows.Children.Clear();
HideGatewayRecovery();
}
private void RenderMessage(string message)
{
- MessagePanel.Children.Clear();
+ MessageBlock.Blocks.Clear();
+ MessageCodeRows.Children.Clear();
+ MessageBlock.Visibility = Visibility.Collapsed;
+
if (string.IsNullOrWhiteSpace(message))
return;
+ var paragraph = new Microsoft.UI.Xaml.Documents.Paragraph();
+ var hasContent = false;
+
foreach (var line in message.Split('\n'))
- AppendLineTo(MessagePanel, line, fontSize: 14, opacity: 0.82);
- }
+ {
+ // Strip leading bullet characters (•, -, *) that look redundant in the UI
+ var trimmed = line.TrimStart();
+ if (trimmed.StartsWith("• ") || trimmed.StartsWith("- ") || trimmed.StartsWith("* "))
+ trimmed = trimmed[2..];
- // Renders a single line into a target panel, decorating URLs as hyperlinks
- // and "Code: XXX" patterns as monospace rows with a copy button.
- private void AppendLineTo(Panel target, string line, double fontSize, double opacity)
- {
- var segment = WizardMessageFormatting.ClassifyLine(line);
+ var segment = WizardMessageFormatting.ClassifyLine(trimmed);
- if (segment.Kind == WizardLineKind.Code)
- {
- target.Children.Add(BuildCodeRow(segment.Prefix, segment.Highlight));
- return;
- }
+ if (segment.Kind == WizardLineKind.Code)
+ {
+ // Code rows with copy buttons stay as separate interactive elements
+ MessageCodeRows.Children.Add(BuildCodeRow(segment.Prefix, segment.Highlight));
+ continue;
+ }
- if (segment.Kind == WizardLineKind.Url && Uri.TryCreate(segment.Highlight, UriKind.Absolute, out var uri))
- {
- target.Children.Add(BuildLinkLine(segment.Text, segment.Highlight, uri));
- return;
+ if (hasContent)
+ paragraph.Inlines.Add(new LineBreak());
+
+ if (segment.Kind == WizardLineKind.Url && Uri.TryCreate(segment.Highlight, UriKind.Absolute, out var uri))
+ {
+ var urlIndex = segment.Text.IndexOf(segment.Highlight, StringComparison.Ordinal);
+ var prefix = segment.Text[..urlIndex];
+ if (!string.IsNullOrEmpty(prefix))
+ paragraph.Inlines.Add(new Run { Text = prefix });
+
+ var link = new Hyperlink { NavigateUri = uri };
+ link.Inlines.Add(new Run { Text = segment.Highlight });
+ paragraph.Inlines.Add(link);
+
+ var suffix = segment.Text[(urlIndex + segment.Highlight.Length)..];
+ if (!string.IsNullOrEmpty(suffix))
+ paragraph.Inlines.Add(new Run { Text = suffix });
+ }
+ else
+ {
+ paragraph.Inlines.Add(new Run { Text = segment.Text });
+ }
+
+ hasContent = true;
}
- target.Children.Add(new TextBlock
+ if (hasContent)
{
- Text = segment.Text,
- FontSize = fontSize,
- FontFamily = new FontFamily("Consolas"),
- Opacity = opacity,
- TextWrapping = TextWrapping.Wrap,
- IsTextSelectionEnabled = true
- });
+ MessageBlock.Blocks.Add(paragraph);
+ MessageBlock.Visibility = Visibility.Visible;
+ }
}
private void StartConsoleTail()
@@ -904,7 +1123,9 @@ private void AppendConsoleLine(string message)
if (string.IsNullOrWhiteSpace(line))
continue;
- AppendLineTo(ConsoleBannerLines, line, fontSize: 13, opacity: 0.92);
+ var para = new Microsoft.UI.Xaml.Documents.Paragraph();
+ para.Inlines.Add(new Run { Text = line });
+ ConsoleBannerBlock.Blocks.Add(para);
}
ConsoleBanner.Visibility = Visibility.Visible;
@@ -943,6 +1164,7 @@ private void AppendQrConsoleBlock(string message)
private void ClearConsoleBanner()
{
+ ConsoleBannerBlock.Blocks.Clear();
ConsoleBannerLines.Children.Clear();
ConsoleBanner.Visibility = Visibility.Collapsed;
}
diff --git a/src/OpenClaw.SetupEngine.UI/SetupWindow.xaml.cs b/src/OpenClaw.SetupEngine.UI/SetupWindow.xaml.cs
index 46c69bbc9..aadb3151c 100644
--- a/src/OpenClaw.SetupEngine.UI/SetupWindow.xaml.cs
+++ b/src/OpenClaw.SetupEngine.UI/SetupWindow.xaml.cs
@@ -161,6 +161,7 @@ public SetupWindow(
NavigateTo(typeof(SecurityNoticePage), _config);
}
+ public void NavigateToSecurityNotice(bool back = false) => NavigateTo(typeof(SecurityNoticePage), _config, back);
public void NavigateToWelcome(bool back = false) => NavigateTo(typeof(WelcomePage), _config, back);
public void NavigateToAdvancedSetup() => NavigateTo(typeof(AdvancedSetupPage), _config);
public void NavigateToCapabilities() => NavigateTo(typeof(CapabilitiesPage), _config);
diff --git a/tests/OpenClaw.Tray.Tests/AppRefactorContractTests.cs b/tests/OpenClaw.Tray.Tests/AppRefactorContractTests.cs
index 870abb364..91d61fc39 100644
--- a/tests/OpenClaw.Tray.Tests/AppRefactorContractTests.cs
+++ b/tests/OpenClaw.Tray.Tests/AppRefactorContractTests.cs
@@ -606,6 +606,20 @@ public void WizardProgressPolling_UsesStepIdForTimeoutClassification()
Assert.Contains("WizardTimeouts.ForStep(title, message, _stepId)", source);
}
+ [Fact]
+ public void WizardResetInputs_RemovesOverflowMoreButton()
+ {
+ var root = TestRepositoryPaths.GetRepositoryRoot();
+ var source = File.ReadAllText(Path.Combine(root, "src", "OpenClaw.SetupEngine.UI", "Pages", "WizardPage.xaml.cs"));
+ var reset = ExtractMethod(source, "ResetInputs");
+
+ // The "More ▾" overflow button is a sibling of SelectOptions in the shared
+ // StackPanel, so ResetInputs must remove it between steps or it leaks forward.
+ Assert.Contains("_moreOptionsButton", reset);
+ Assert.Contains("morePanel.Children.Remove(_moreOptionsButton)", reset);
+ Assert.Contains("_moreOptionsButton = null", reset);
+ }
+
[Fact]
public void WizardCompletion_AppliesWindowsNodeContextBeforeSummary()
{
@@ -734,9 +748,9 @@ public void SetupWelcomePage_RunsExistingConfigDetectionOffUiThread()
{
var root = TestRepositoryPaths.GetRepositoryRoot();
var source = File.ReadAllText(Path.Combine(root, "src", "OpenClaw.SetupEngine.UI", "Pages", "WelcomePage.xaml.cs"));
- var method = ExtractMethod(source, "StartButtonClickAsync");
+ var method = ExtractMethod(source, "StartInstallAsync");
- Assert.Contains("InstallButton.IsEnabled = false", method);
+ Assert.Contains("NextButton.IsEnabled = false", method);
Assert.Contains("InstallTitle.Text = CheckingButtonText", method);
Assert.Contains("CheckingButtonText", method);
Assert.Contains("var setupWindow = SetupWindow.Active", method);
@@ -744,10 +758,10 @@ public void SetupWelcomePage_RunsExistingConfigDetectionOffUiThread()
Assert.Contains("setupWindow is null or { IsClosed: true } || xamlRoot is null", method);
Assert.Contains("setupWindow is { IsClosed: false }", method);
Assert.Contains("InstallTitle.Text = InstallButtonText", method);
- Assert.Contains("InstallButton.IsEnabled = true", method);
+ Assert.Contains("NextButton.IsEnabled = true", method);
AssertInOrder(
method,
- "InstallButton.IsEnabled = false",
+ "NextButton.IsEnabled = false",
"await Task.Run(() => ExistingConfigDetector.Detect",
"setupWindow is null or { IsClosed: true } || xamlRoot is null",
"dialog.ShowAsync()",