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
2 changes: 1 addition & 1 deletion docs/ONBOARDING_WIZARD.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ The setup flow no longer configures remote/manual gateways inline. The Welcome p
## Screen Details

### Welcome
Displays the OpenClaw icon, app title, and a brief description. If an app-owned local WSL gateway already exists, the primary CTA reads **Install new WSL Gateway** and confirmation warns that the current OpenClaw WSL gateway and distro will be deleted. If only an external gateway exists, the CTA remains **Set up locally** and confirmation explains that the external connection remains available in Connections.
Displays the OpenClaw icon, app title, and a brief description. Choosing local gateway setup runs the read-only WSL readiness gate before the Capabilities page or its Local AI decision UI can open. WSL2 environment failures, including disabled hardware virtualization, are shown as WSL readiness failures and block both Local AI and non-Local-AI local gateway setup. If an app-owned local WSL gateway already exists, the primary CTA reads **Install new WSL Gateway** and confirmation warns that the current OpenClaw WSL gateway and distro will be deleted. If only an external gateway exists, the CTA remains **Set up locally** and confirmation explains that the external connection remains available in Connections.

### Local setup progress
Installs and connects a new app-owned `OpenClawGateway` WSL instance from a clean WSL baseline. Setup does not export from or mutate an existing user Ubuntu distro; if WSL cannot create the named app-owned distro directly, setup fails with an actionable update message. When replacing an app-owned local gateway, the removal step is shown as part of progress and can be retried on failure.
Expand Down
2 changes: 1 addition & 1 deletion docs/SETUP_ENGINE_REDESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ Log path defaults to `%APPDATA%\OpenClawTray\Logs\Setup\setup-engine-<yyyyMMdd-H

The WinUI app is a **thin shell** - no business logic, just rendering pipeline state. End-user UI runs default to `RollbackOnFailure=true`; `--no-rollback-on-failure` preserves an explicit debugging opt-out.

### Page Flow: Security → Welcome → Capabilities → Progress → OpenClaw onboard → Complete
### Page Flow: Security → Welcome → WSL readiness gate → Capabilities → Progress → OpenClaw onboard → Complete

**SecurityNoticePage**
- Native warning InfoBar for device-trust and setup transparency
Expand Down
26 changes: 0 additions & 26 deletions src/OpenClaw.SetupEngine.UI/Pages/CapabilitiesPage.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -269,9 +269,6 @@ private async Task InitializeLocalAiReviewAsync(bool forceNetworkingConsent)
Task<HostHardwareInfo> hardwareTask = setupWindow is not null
? setupWindow.GetLocalAiHardwareAsync()
: Task.Run(() => new NvmlHostHardwareProbe().Probe());
Task<WslViabilityResult> wslTask = setupWindow is not null
? setupWindow.GetWslViabilityAsync()
: InspectWslViabilityAsync();

string? hardwareReason = null;
LocalInferenceEligibilityResult? eligibility = null;
Expand All @@ -297,19 +294,6 @@ _localAiSelectedGpuCapacityBytes is { } capacityBytes &&
"Check the NVIDIA driver installation and try setup again.";
}

WslViabilityResult wslViability;
try
{
wslViability = await wslTask;
}
catch
{
wslViability = new(
WslViabilityKind.InspectionFailed,
"OpenClaw could not safely verify the WSL2 environment.",
"Run wsl --status in PowerShell, resolve the reported problem, and try setup again.");
}

string? wslNetworkingReason = null;
try
{
Expand All @@ -330,7 +314,6 @@ _localAiSelectedGpuCapacityBytes is { } capacityBytes &&

string? unavailableReason = LocalAiAvailabilityReasons.Build(
hardwareReason,
wslViability,
wslNetworkingReason);
if (unavailableReason is not null)
{
Expand All @@ -352,15 +335,6 @@ _localAiSelectedGpuCapacityBytes is { } capacityBytes &&
ApplySetupReviewSummary(_config);
}

private static async Task<WslViabilityResult> InspectWslViabilityAsync()
{
using var logger = new SetupLogger(filePath: null);
return await WslViabilityInspector.InspectAsync(
new CommandRunner(logger),
logger,
CancellationToken.None);
}

private static WslGlobalConfigManager CreateWslGlobalConfigManager()
{
var profile = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
Expand Down
2 changes: 1 addition & 1 deletion src/OpenClaw.SetupEngine.UI/Pages/WelcomePage.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
VerticalAlignment="Center" />
<ProgressRing x:Name="InstallCheckProgress"
AutomationProperties.AutomationId="WelcomeInstallCheckProgress"
AutomationProperties.Name="Checking existing WSL setup"
AutomationProperties.Name="Checking WSL readiness and existing setup"
Width="16" Height="16"
IsActive="False" Visibility="Collapsed" />
<Border CornerRadius="4" Padding="7,1" VerticalAlignment="Center"
Expand Down
32 changes: 28 additions & 4 deletions src/OpenClaw.SetupEngine.UI/Pages/WelcomePage.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ private async Task DetectLocalAiAvailabilityAsync()
if (setupWindow is null || config is null)
return;

WslViabilityResult wslViability = await setupWindow.GetWslViabilityAsync();
if (wslViability.BlocksSetup)
return;

var hardware = await setupWindow.GetLocalAiHardwareAsync();
if (!IsLoaded || !ReferenceEquals(SetupWindow.Active, setupWindow))
return;
Expand Down Expand Up @@ -143,7 +147,10 @@ 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();
if (setupWindow is null)
return;

var dataDir = setupWindow.DataDir;

NextButton.IsEnabled = false;
InstallTitle.Text = CheckingButtonText;
Expand All @@ -152,6 +159,23 @@ private async Task StartInstallAsync()
var navigating = false;
try
{
WslViabilityResult wslViability = await setupWindow.GetWslViabilityAsync();
if (wslViability.BlocksSetup)
{
var readinessRoot = XamlRoot;
if (!setupWindow.IsClosed && readinessRoot is not null)
{
await new ContentDialog
{
Title = "WSL2 is not ready",
Content = wslViability.Description,
CloseButtonText = "Close",
XamlRoot = readinessRoot,
}.ShowAsync();
}
return;
}

ExistingConfigDetector.ExistingConfig existing;
try
{
Expand All @@ -160,7 +184,7 @@ private async Task StartInstallAsync()
catch (InvalidOperationException ex)
{
var errorRoot = XamlRoot;
if (setupWindow is not null and { IsClosed: false } && errorRoot is not null)
if (!setupWindow.IsClosed && errorRoot is not null)
{
await new ContentDialog
{
Expand All @@ -174,7 +198,7 @@ private async Task StartInstallAsync()
}

var xamlRoot = XamlRoot;
if (setupWindow is null or { IsClosed: true } || xamlRoot is null)
if (setupWindow.IsClosed || xamlRoot is null)
return;

InstallTitle.Text = InstallButtonText;
Expand Down Expand Up @@ -203,7 +227,7 @@ private async Task StartInstallAsync()
}
finally
{
if (!navigating && setupWindow is { IsClosed: false })
if (!navigating && !setupWindow.IsClosed)
{
InstallTitle.Text = InstallButtonText;
InstallCheckProgress.IsActive = false;
Expand Down
6 changes: 1 addition & 5 deletions src/OpenClaw.SetupEngine/LocalAiAvailabilityReasons.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,12 @@ internal static class LocalAiAvailabilityReasons
{
public static string? Build(
string? hardwareReason,
WslViabilityResult wslViability,
string? wslNetworkingReason)
{
ArgumentNullException.ThrowIfNull(wslViability);
var reasons = new List<string>(capacity: 3);
var reasons = new List<string>(capacity: 2);

if (!string.IsNullOrWhiteSpace(hardwareReason))
reasons.Add($"Hardware: {hardwareReason.Trim()}");
if (wslViability.BlocksSetup)
reasons.Add($"WSL: {wslViability.Description}");
if (!string.IsNullOrWhiteSpace(wslNetworkingReason))
reasons.Add($"WSL networking: {wslNetworkingReason.Trim()}");

Expand Down
6 changes: 6 additions & 0 deletions tests/OpenClaw.SetupEngine.Tests/SetupPipelineTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,12 @@ public void BuildDefaultSteps_IncludesCurrentSetupFlow()
Assert.IsType<StartKeepaliveStep>(steps[^1]);

var ensureWslIndex = steps.FindIndex(step => step is EnsureWslPlatformStep);
var preflightWslIndex = steps.FindIndex(step => step is PreflightWslStep);
var localAiHardwareIndex = steps.FindIndex(step => step is PreflightLocalAiHardwareStep);
var runtimeDownloadIndex = steps.FindIndex(step => step is AcquireLocalAiRuntimeStep);
var modelDownloadIndex = steps.FindIndex(step => step is AcquireLocalAiModelStep);
Assert.True(localAiHardwareIndex < preflightWslIndex);
Assert.True(preflightWslIndex < ensureWslIndex);
Assert.True(ensureWslIndex < runtimeDownloadIndex);
Assert.True(ensureWslIndex < modelDownloadIndex);
}
Expand Down Expand Up @@ -147,6 +151,8 @@ public void LocalAiDisabled_SkipsEveryLocalAiMutation()
];

Assert.All(localAiSteps, step => Assert.True(step.CanSkip(ctx), step.Id));
Assert.False(steps.Single(step => step is PreflightWslStep).CanSkip(ctx));
Assert.False(steps.Single(step => step is EnsureWslPlatformStep).CanSkip(ctx));
}

[Theory]
Expand Down
45 changes: 30 additions & 15 deletions tests/OpenClaw.SetupEngine.Tests/SetupStepsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1295,33 +1295,22 @@ public async Task PreflightWsl_UnclassifiedStatusFailureFailsClosed()
}

[Fact]
public void LocalAiAvailabilityReasons_CombinesHardwareWslAndNetworkingFailures()
public void LocalAiAvailabilityReasons_CombinesOnlyLocalAiFailures()
{
var wsl = new WslViabilityResult(
WslViabilityKind.EnvironmentBlocked,
"Windows cannot currently start WSL2.",
"Enable virtualization and Virtual Machine Platform.");

var result = LocalAiAvailabilityReasons.Build(
"No qualified NVIDIA GPU was detected.",
wsl,
"The global .wslconfig file is unreadable.");

Assert.NotNull(result);
Assert.Contains("Hardware: No qualified NVIDIA GPU was detected.", result);
Assert.Contains("WSL: Windows cannot currently start WSL2.", result);
Assert.Contains("WSL networking: The global .wslconfig file is unreadable.", result);
Assert.DoesNotContain("Windows cannot currently start WSL2", result);
}

[Fact]
public void LocalAiAvailabilityReasons_DoesNotBlockForInstallableWsl()
public void LocalAiAvailabilityReasons_ReturnsNullWithoutLocalAiFailures()
{
var wsl = new WslViabilityResult(
WslViabilityKind.Installable,
"WSL is not installed yet.",
"Setup can install it later.");

Assert.Null(LocalAiAvailabilityReasons.Build(null, wsl, null));
Assert.Null(LocalAiAvailabilityReasons.Build(null, null));
}

[Fact]
Expand Down Expand Up @@ -1866,11 +1855,37 @@ public async Task PreflightWsl_FailsTerminalWhenVirtualizationDisabledInFirmware
var result = await new PreflightWslStep().ExecuteAsync(ctx, CancellationToken.None);

Assert.Equal(StepOutcome.FailedTerminal, result.Outcome);
Assert.Equal(WslViabilityKind.EnvironmentBlocked, ctx.WslViability?.Kind);
Assert.StartsWith("Windows cannot currently start WSL2.", result.Message);
Assert.Contains("virtualization", result.Message, StringComparison.OrdinalIgnoreCase);
Assert.DoesNotContain("Local AI", result.Message, StringComparison.OrdinalIgnoreCase);
// Don't assert on "BIOS" / "UEFI" here -- the wording flexes by host
// CPU architecture (this test runs on either x64 or Arm64 dev boxes).
}

[Fact]
public async Task PreflightWsl_VirtualizationFailureBlocksWhenLocalAiIsDisabled()
{
var commands = new FakeCommandRunner(args => args switch
{
["--version"] => Ok("WSL version: 2.7.3.0\n"),
["--status"] => Ok(
"WSL2 is unable to start since virtualization is not enabled on this machine. "
+ "Turn on virtualization in firmware settings."),
_ => Fail($"unexpected args: {string.Join(' ', args)}"),
});
var ctx = CreateContext(
new SetupConfig { LocalAi = new LocalAiConfig { Enabled = false } },
commands);

var result = await new PreflightWslStep().ExecuteAsync(ctx, CancellationToken.None);

Assert.False(new PreflightWslStep().CanSkip(ctx));
Assert.Equal(StepOutcome.FailedTerminal, result.Outcome);
Assert.Equal(WslViabilityKind.EnvironmentBlocked, ctx.WslViability?.Kind);
Assert.DoesNotContain("Local AI", result.Message, StringComparison.OrdinalIgnoreCase);
}

[Fact]
public async Task PreflightWsl_FailsTerminalWhenWslEmitsHcsServiceNotAvailable()
{
Expand Down
46 changes: 41 additions & 5 deletions tests/OpenClaw.Tray.Tests/AppRefactorContractTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1087,7 +1087,7 @@ public void Setup_IsHostedInTrayAndUsesSelfRestartAfterCompletion()
Assert.Contains("config.SkipWizard || step is not WindowsNodeBootstrapContextStep", progressPage);
Assert.Contains("_dataDir,", progressPage);
Assert.Contains("_localDataDir);", progressPage);
Assert.Contains("setupWindow?.DataDir ?? SetupContext.ResolveDataDir()", welcomePage);
Assert.Contains("var dataDir = setupWindow.DataDir", welcomePage);
Assert.Contains("SetupWindow.Active?.DataDir ?? SetupContext.ResolveDataDir()", wizardPage);
Assert.Contains("await CompleteSetupAsync(generation)", wizardPage);
Assert.Contains("ApplyWindowsNodeContextAsync", wizardPage);
Expand Down Expand Up @@ -1181,6 +1181,41 @@ public void SetupProgress_PreparesWslBeforeLocalAiDownloads()
Assert.DoesNotContain("Verify Local AI before WSL setup", code);
}

[Fact]
public void SetupWelcome_BlocksOnWslReadinessBeforeLocalAiDecisionUi()
{
var root = TestRepositoryPaths.GetRepositoryRoot();
var welcome = File.ReadAllText(Path.Combine(
root,
"src",
"OpenClaw.SetupEngine.UI",
"Pages",
"WelcomePage.xaml.cs"));
var capabilities = File.ReadAllText(Path.Combine(
root,
"src",
"OpenClaw.SetupEngine.UI",
"Pages",
"CapabilitiesPage.xaml.cs"));
var startInstall = ExtractMethod(welcome, "StartInstallAsync");
var detectLocalAi = ExtractMethod(welcome, "DetectLocalAiAvailabilityAsync");

AssertInOrder(
startInstall,
"GetWslViabilityAsync()",
"if (wslViability.BlocksSetup)",
"Title = \"WSL2 is not ready\"",
"ExistingConfigDetector.Detect",
"NavigateToCapabilities()");
AssertInOrder(
detectLocalAi,
"GetWslViabilityAsync()",
"if (wslViability.BlocksSetup)",
"GetLocalAiHardwareAsync()");
Assert.DoesNotContain("GetWslViabilityAsync", capabilities);
Assert.DoesNotContain("WslViabilityKind", capabilities);
}

[Fact]
public void SetupCompletion_PersistsStartupChoiceBeforeRestart()
{
Expand Down Expand Up @@ -1352,7 +1387,7 @@ public void CapabilitiesPage_DisclosesAlwaysOnDeviceStatusWithoutOfferingFalseTo
}

[Fact]
public void CapabilitiesPage_AggregatesHardwareAndWslLocalAiDiagnosis()
public void CapabilitiesPage_AggregatesOnlyLocalAiHardwareAndNetworkingDiagnosis()
{
var root = TestRepositoryPaths.GetRepositoryRoot();
var source = File.ReadAllText(Path.Combine(root, "src", "OpenClaw.SetupEngine.UI", "Pages", "CapabilitiesPage.xaml.cs"));
Expand All @@ -1361,6 +1396,7 @@ public void CapabilitiesPage_AggregatesHardwareAndWslLocalAiDiagnosis()
Assert.Contains("Why Local AI is unavailable", source);
Assert.Contains("LocalAiInstallReviewCard.Visibility = Visibility.Visible", ExtractMethod(source, "ShowLocalAiUnavailable"));
Assert.Contains("LocalAiAvailabilityReasons.Build", source);
Assert.DoesNotContain("WslViability", source);
Assert.Contains("One or more Local AI requirements are unavailable.", xaml);
Assert.Matches(
new Regex(
Expand Down Expand Up @@ -1578,8 +1614,8 @@ public void SetupWelcomePage_RunsExistingConfigDetectionOffUiThread()
Assert.Contains("CheckingButtonText", method);
Assert.Contains("var setupWindow = SetupWindow.Active", method);
Assert.Contains("await Task.Run(() => ExistingConfigDetector.Detect", method);
Assert.Contains("setupWindow is null or { IsClosed: true } || xamlRoot is null", method);
Assert.Contains("setupWindow is { IsClosed: false }", method);
Assert.Contains("setupWindow.IsClosed || xamlRoot is null", method);
Assert.Contains("!setupWindow.IsClosed", method);
Assert.Contains("InstallTitle.Text = InstallButtonText", method);
Assert.Contains("InstallCheckProgress.IsActive = false", method);
Assert.Contains("InstallCheckProgress.Visibility = Visibility.Collapsed", method);
Expand All @@ -1590,7 +1626,7 @@ public void SetupWelcomePage_RunsExistingConfigDetectionOffUiThread()
method,
"NextButton.IsEnabled = false",
"await Task.Run(() => ExistingConfigDetector.Detect",
"setupWindow is null or { IsClosed: true } || xamlRoot is null",
"setupWindow.IsClosed || xamlRoot is null",
"dialog.ShowAsync()",
"setupWindow.NavigateToCapabilities()");
}
Expand Down
Loading