diff --git a/docs/ONBOARDING_WIZARD.md b/docs/ONBOARDING_WIZARD.md index d2e3944a6..79443d607 100644 --- a/docs/ONBOARDING_WIZARD.md +++ b/docs/ONBOARDING_WIZARD.md @@ -38,6 +38,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. +When the gateway config wizard surfaces an error and the active gateway is an app-managed WSL distro, the error state also offers **Open terminal** and **Restart gateway**. The wizard does not parse or classify the gateway's error text; it leaves the message visible and selectable so the user can copy any command the gateway reports. The buttons reuse the shared `GatewayTerminalLauncher` and `WslGatewayController` (in `OpenClaw.Connection`, also used by the Connections tab). Restart re-enters the gateway config wizard (the provider/model onboarding step — not the whole V2 onboarding, and without re-installing the WSL distro) so fixes such as newly-installed tools are picked up on `PATH`. Because the gateway restart clears its wizard session, this resumes at the first config question rather than the exact step that failed. Detection is gated on `GatewayRecord.SetupManagedDistroName`, so it never appears for remote/SSH gateways. + ### Permissions Checks 5 Windows permissions using native APIs and registry: - Notifications (Toast capability) diff --git a/src/OpenClaw.Tray.WinUI/Services/GatewayHostAccess.cs b/src/OpenClaw.Connection/GatewayHostAccess.cs similarity index 93% rename from src/OpenClaw.Tray.WinUI/Services/GatewayHostAccess.cs rename to src/OpenClaw.Connection/GatewayHostAccess.cs index 8967457e7..77e94bd45 100644 --- a/src/OpenClaw.Tray.WinUI/Services/GatewayHostAccess.cs +++ b/src/OpenClaw.Connection/GatewayHostAccess.cs @@ -1,9 +1,8 @@ using System; -using OpenClaw.Connection; -namespace OpenClawTray.Services; +namespace OpenClaw.Connection; -internal enum GatewayTerminalTarget +public enum GatewayTerminalTarget { None, Wsl, @@ -16,13 +15,13 @@ internal enum GatewayTerminalTarget /// without a WinUI runtime. App.xaml.cs wires these up to LocalizationHelper /// at startup so the running app sees real localized strings. /// -internal static class GatewayHostAccessLocalization +public static class GatewayHostAccessLocalization { public static Func GetString { get; set; } = key => key; public static Func Format { get; set; } = (key, _) => key; } -internal sealed record GatewayHostAccessPlan( +public sealed record GatewayHostAccessPlan( string? GatewayId, GatewayTerminalTarget TerminalTarget, string? DistroName, @@ -53,7 +52,7 @@ public static GatewayHostAccessPlan None(string? gatewayId = null, string? disab } } -internal static class GatewayHostAccessClassifier +public static class GatewayHostAccessClassifier { public static GatewayHostAccessPlan Classify(GatewayRecord? record) { @@ -104,4 +103,4 @@ public static GatewayHostAccessPlan Classify(GatewayRecord? record) var trimmed = value?.Trim(); return string.IsNullOrWhiteSpace(trimmed) ? null : trimmed; } -} +} \ No newline at end of file diff --git a/src/OpenClaw.Tray.WinUI/Services/GatewayTerminalLauncher.cs b/src/OpenClaw.Connection/GatewayTerminalLauncher.cs similarity index 96% rename from src/OpenClaw.Tray.WinUI/Services/GatewayTerminalLauncher.cs rename to src/OpenClaw.Connection/GatewayTerminalLauncher.cs index 7c6280b40..06f21696b 100644 --- a/src/OpenClaw.Tray.WinUI/Services/GatewayTerminalLauncher.cs +++ b/src/OpenClaw.Connection/GatewayTerminalLauncher.cs @@ -2,21 +2,21 @@ using System.Diagnostics; using OpenClaw.Shared; -namespace OpenClawTray.Services; +namespace OpenClaw.Connection; -internal interface IGatewayTerminalLauncher +public interface IGatewayTerminalLauncher { void Open(GatewayHostAccessPlan accessPlan); void OpenGatewayDoctor(GatewayHostAccessPlan accessPlan); } -internal sealed record GatewayTerminalLaunchCommand( +public sealed record GatewayTerminalLaunchCommand( string FileName, IReadOnlyList Arguments, bool UsesWindowsTerminal); -internal static class GatewayTerminalLaunchCommandBuilder +public static class GatewayTerminalLaunchCommandBuilder { public static GatewayTerminalLaunchCommand Build(GatewayHostAccessPlan accessPlan, string? windowsTerminalPath) { @@ -144,7 +144,7 @@ private static string RequireValue(string? value, string message) } } -internal sealed class GatewayTerminalLauncher(IOpenClawLogger logger) : IGatewayTerminalLauncher +public sealed class GatewayTerminalLauncher(IOpenClawLogger logger) : IGatewayTerminalLauncher { public void Open(GatewayHostAccessPlan accessPlan) { diff --git a/src/OpenClaw.Tray.WinUI/Services/WslCommandRunner.cs b/src/OpenClaw.Connection/WslCommandRunner.cs similarity index 99% rename from src/OpenClaw.Tray.WinUI/Services/WslCommandRunner.cs rename to src/OpenClaw.Connection/WslCommandRunner.cs index 0bc4ad9ae..8dfd3ef50 100644 --- a/src/OpenClaw.Tray.WinUI/Services/WslCommandRunner.cs +++ b/src/OpenClaw.Connection/WslCommandRunner.cs @@ -3,7 +3,7 @@ using System.Globalization; using System.Text; -namespace OpenClawTray.Services; +namespace OpenClaw.Connection; public sealed record WslCommandResult(int ExitCode, string StandardOutput, string StandardError) { diff --git a/src/OpenClaw.Tray.WinUI/Services/WslGatewayController.cs b/src/OpenClaw.Connection/WslGatewayController.cs similarity index 78% rename from src/OpenClaw.Tray.WinUI/Services/WslGatewayController.cs rename to src/OpenClaw.Connection/WslGatewayController.cs index 4db6ec314..b28dd3824 100644 --- a/src/OpenClaw.Tray.WinUI/Services/WslGatewayController.cs +++ b/src/OpenClaw.Connection/WslGatewayController.cs @@ -1,7 +1,7 @@ using System.Collections.ObjectModel; using OpenClaw.Shared; -namespace OpenClawTray.Services; +namespace OpenClaw.Connection; public enum WslGatewayControlAction { @@ -10,7 +10,7 @@ public enum WslGatewayControlAction Restart } -internal sealed record WslGatewayControlResult( +public sealed record WslGatewayControlResult( string DistroName, WslGatewayControlAction Action, int ExitCode, @@ -29,7 +29,7 @@ public string OutputSummary } } -internal static class WslGatewayControlCommandBuilder +public static class WslGatewayControlCommandBuilder { internal const string OpenClawWslPathPrefix = "export PATH=\"/home/openclaw/.openclaw/bin:/opt/openclaw/bin:/usr/local/bin:$PATH\""; @@ -54,7 +54,7 @@ public static string ToVerb(WslGatewayControlAction action) } } -internal sealed class WslGatewayController(IWslCommandRunner commandRunner, IOpenClawLogger logger) +public sealed class WslGatewayController(IWslCommandRunner commandRunner, IOpenClawLogger logger) { public async Task RunAsync( string distroName, @@ -68,7 +68,14 @@ public async Task RunAsync( var normalizedDistroName = distroName.Trim(); var distros = await commandRunner.ListDistrosAsync(cancellationToken).ConfigureAwait(false); - if (!distros.Any(distro => string.Equals(distro.Name, normalizedDistroName, StringComparison.OrdinalIgnoreCase))) + + // Only short-circuit as "not registered" when the probe returned a non-empty + // enumeration that definitively lacks the distro. An empty list is ambiguous: + // `wsl --list` may have failed or timed out (ListDistrosAsync collapses any + // failure to an empty list), so fail open and let the actual control command + // surface the real error instead of dead-ending recovery with a misleading message. + if (distros.Count > 0 && + !distros.Any(distro => string.Equals(distro.Name, normalizedDistroName, StringComparison.OrdinalIgnoreCase))) { return new WslGatewayControlResult( normalizedDistroName, diff --git a/src/OpenClaw.SetupEngine.UI/Pages/WizardPage.xaml b/src/OpenClaw.SetupEngine.UI/Pages/WizardPage.xaml index 1097c29bf..e349a95fb 100644 --- a/src/OpenClaw.SetupEngine.UI/Pages/WizardPage.xaml +++ b/src/OpenClaw.SetupEngine.UI/Pages/WizardPage.xaml @@ -42,7 +42,26 @@ - + + +