diff --git a/src/OpenClaw.SetupEngine.UI/Pages/CapabilitiesPage.xaml b/src/OpenClaw.SetupEngine.UI/Pages/CapabilitiesPage.xaml
index 05a4e49c9..0357aac61 100644
--- a/src/OpenClaw.SetupEngine.UI/Pages/CapabilitiesPage.xaml
+++ b/src/OpenClaw.SetupEngine.UI/Pages/CapabilitiesPage.xaml
@@ -114,6 +114,21 @@
+
+
+
+
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/OpenClaw.SetupEngine.UI/Pages/CapabilitiesPage.xaml.cs b/src/OpenClaw.SetupEngine.UI/Pages/CapabilitiesPage.xaml.cs
index 81ed218ce..2242a20c5 100644
--- a/src/OpenClaw.SetupEngine.UI/Pages/CapabilitiesPage.xaml.cs
+++ b/src/OpenClaw.SetupEngine.UI/Pages/CapabilitiesPage.xaml.cs
@@ -1,5 +1,6 @@
using Microsoft.UI;
using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Automation;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Navigation;
@@ -342,6 +343,7 @@ _localAiSelectedGpuCapacityBytes is { } capacityBytes &&
LocalAiInstallReviewCard.Visibility = Visibility.Visible;
LocalAiUnavailablePanel.Visibility = Visibility.Collapsed;
LocalAiToggle.Visibility = Visibility.Visible;
+ SetLocalAiOptionAvailability(isAvailable: true);
_localAiSelectionEligible = eligibility.Status == LocalInferenceEligibilityStatus.Eligible;
_config!.LocalAi.SelectedModelId ??= eligibility.Plan!.Model.Id;
PopulateLocalAiModels();
@@ -377,41 +379,31 @@ private void ShowLocalAiUnavailable(string reason)
_suppressLocalAiToggle = true;
LocalAiToggle.IsOn = false;
_suppressLocalAiToggle = false;
- LocalAiToggle.Visibility = Visibility.Collapsed;
+ LocalAiToggle.Visibility = Visibility.Visible;
LocalAiDetailsPanel.Visibility = Visibility.Collapsed;
_localAiUnavailableReason = reason;
LocalAiUnavailablePanel.Visibility = Visibility.Visible;
LocalAiInstallReviewCard.Visibility = Visibility.Visible;
+ SetLocalAiOptionAvailability(isAvailable: false);
_config!.LocalAi.Enabled = false;
_config.SkipWizard = _skipWizardWithoutLocalAi;
ApplySetupReviewSummary(_config);
}
private static string DescribeLocalAiUnavailable(LocalInferenceEligibilityResult eligibility) =>
- eligibility.SelectionFailureCode switch
- {
- LocalInferenceSelectionFailureCode.RuntimeUnavailable =>
- "This Local AI release does not include a native llama-server runtime for the detected Windows architecture.",
- LocalInferenceSelectionFailureCode.NoNvidiaGpu =>
- "No NVIDIA GPU was reported by the NVIDIA driver. Install or repair the NVIDIA driver, then try setup again.",
- LocalInferenceSelectionFailureCode.UnknownModel =>
- "The selected model is not available in this Local AI release.",
- _ => eligibility.FailureCode switch
- {
- LocalInferenceEligibilityFailureCode.HardwareFactsIncomplete =>
- "OpenClaw could not read a stable NVIDIA GPU identifier, memory, driver, or CUDA capability.",
- LocalInferenceEligibilityFailureCode.InsufficientGpuMemory =>
- $"{eligibility.Plan?.Model.DisplayName ?? "The selected model"} requires " +
- $"{FormatSize(eligibility.RequiredTotalMemoryBytes)} of GPU memory for model weights, KV cache, and runtime workspace. " +
- $"OpenClaw detected {FormatOptionalSize(eligibility.DetectedTotalMemoryBytes)}.",
- LocalInferenceEligibilityFailureCode.DriverTooOld =>
- $"NVIDIA driver {eligibility.SelectedGpu?.DriverVersion ?? "unknown"} was detected. " +
- $"Local AI requires version {LocalInferenceEligibility.MinimumNvidiaDriverVersion} or newer.",
- LocalInferenceEligibilityFailureCode.CudaCapabilityTooLow =>
- "The NVIDIA driver does not provide CUDA 13 support. A separate CUDA Toolkit is not required.",
- _ => "OpenClaw could not verify the Local AI requirements on this system.",
- },
- };
+ LocalInferenceEligibilityDiagnostics.DescribeUnavailable(eligibility);
+
+ private void SetLocalAiOptionAvailability(bool isAvailable)
+ {
+ LocalAiOptionContent.IsHitTestVisible = isAvailable;
+ LocalAiOptionContent.Opacity = isAvailable ? 1 : 0.55;
+ LocalAiToggle.IsEnabled = isAvailable;
+ LocalAiModelSelector.IsEnabled = isAvailable;
+ LocalAiNetworkingConsentCheckBox.IsEnabled = isAvailable;
+ AutomationProperties.SetHelpText(
+ LocalAiOptionContent,
+ isAvailable ? string.Empty : "Unavailable because this PC does not meet the Local AI requirements.");
+ }
private void LocalAiUnavailableDetails_Click(object sender, RoutedEventArgs e) =>
AsyncEventHandlerGuard.Run(
diff --git a/src/OpenClaw.SetupEngine.UI/Pages/WelcomePage.xaml b/src/OpenClaw.SetupEngine.UI/Pages/WelcomePage.xaml
index 96d0ca872..700140f92 100644
--- a/src/OpenClaw.SetupEngine.UI/Pages/WelcomePage.xaml
+++ b/src/OpenClaw.SetupEngine.UI/Pages/WelcomePage.xaml
@@ -65,30 +65,27 @@
Style="{StaticResource CaptionTextBlockStyle}"
Foreground="{ThemeResource AccentTextFillColorPrimaryBrush}" />
+
+
+
+
+
+
-
-
-
-
-
-
-
diff --git a/src/OpenClaw.SetupEngine.UI/Pages/WelcomePage.xaml.cs b/src/OpenClaw.SetupEngine.UI/Pages/WelcomePage.xaml.cs
index 7c74afe20..b6e10166d 100644
--- a/src/OpenClaw.SetupEngine.UI/Pages/WelcomePage.xaml.cs
+++ b/src/OpenClaw.SetupEngine.UI/Pages/WelcomePage.xaml.cs
@@ -1,5 +1,6 @@
using Microsoft.UI.Composition;
using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Automation;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Hosting;
using Microsoft.UI.Xaml.Navigation;
@@ -63,12 +64,13 @@ private async Task DetectLocalAiAvailabilityAsync()
LocalInferenceEligibilityResult eligibility = LocalInferenceEligibility.Evaluate(
hardware,
config.LocalAi.SelectedModelId);
- if (!eligibility.CanInstall || eligibility.SelectedGpu is not { } gpu)
+ if (!eligibility.CanInstall || eligibility.SelectedGpu is null)
return;
- LocalAiAvailabilityText.Text =
- $"{gpu.Name} detected. Install a local gateway to use local AI inference on this PC.";
- LocalAiAvailabilityPanel.Visibility = Visibility.Visible;
+ LocalAiAvailabilityBadge.Visibility = Visibility.Visible;
+ AutomationProperties.SetName(
+ InstallChoice,
+ "Install a local gateway (WSL), recommended, Local AI available");
}
private void StartMascotBreatheAnimation()
diff --git a/src/OpenClaw.Shared/Inference/Catalog/LocalInferenceEligibilityDiagnostics.cs b/src/OpenClaw.Shared/Inference/Catalog/LocalInferenceEligibilityDiagnostics.cs
new file mode 100644
index 000000000..e5f3a787f
--- /dev/null
+++ b/src/OpenClaw.Shared/Inference/Catalog/LocalInferenceEligibilityDiagnostics.cs
@@ -0,0 +1,40 @@
+namespace OpenClaw.Shared.Inference.Catalog;
+
+public static class LocalInferenceEligibilityDiagnostics
+{
+ public static string DescribeUnavailable(LocalInferenceEligibilityResult eligibility)
+ {
+ ArgumentNullException.ThrowIfNull(eligibility);
+
+ return eligibility.SelectionFailureCode switch
+ {
+ LocalInferenceSelectionFailureCode.RuntimeUnavailable =>
+ "This Local AI release does not include a native llama-server runtime for the detected Windows architecture.",
+ LocalInferenceSelectionFailureCode.NoNvidiaGpu =>
+ "No NVIDIA GPU was reported by the NVIDIA driver. Install or repair the NVIDIA driver, then try setup again.",
+ LocalInferenceSelectionFailureCode.UnknownModel =>
+ "The selected model is not available in this Local AI release.",
+ _ => eligibility.FailureCode switch
+ {
+ LocalInferenceEligibilityFailureCode.HardwareFactsIncomplete =>
+ "OpenClaw could not read a stable NVIDIA GPU identifier, memory, driver, or CUDA capability.",
+ LocalInferenceEligibilityFailureCode.InsufficientGpuMemory =>
+ $"{eligibility.Plan?.Model.DisplayName ?? "The selected model"} requires " +
+ $"{FormatSize(eligibility.RequiredTotalMemoryBytes)} of GPU memory for model weights, KV cache, and runtime workspace. " +
+ $"OpenClaw detected {FormatOptionalSize(eligibility.DetectedTotalMemoryBytes)}.",
+ LocalInferenceEligibilityFailureCode.DriverTooOld =>
+ $"NVIDIA driver {eligibility.SelectedGpu?.DriverVersion ?? "unknown"} was detected. " +
+ $"Local AI requires version {LocalInferenceEligibility.MinimumNvidiaDriverVersion} or newer.",
+ LocalInferenceEligibilityFailureCode.CudaCapabilityTooLow =>
+ "The NVIDIA driver does not provide CUDA 13 support. A separate CUDA Toolkit is not required.",
+ _ => "OpenClaw could not verify the Local AI requirements on this system.",
+ },
+ };
+ }
+
+ private static string FormatSize(long bytes) =>
+ $"{bytes / 1_000_000_000d:0.#} GB";
+
+ private static string FormatOptionalSize(long? bytes) =>
+ bytes is { } value ? FormatSize(value) : "an unknown amount";
+}
diff --git a/src/OpenClaw.Tray.WinUI/Pages/LocalAiPage.xaml b/src/OpenClaw.Tray.WinUI/Pages/LocalAiPage.xaml
index 83157c6b6..cef53e1fa 100644
--- a/src/OpenClaw.Tray.WinUI/Pages/LocalAiPage.xaml
+++ b/src/OpenClaw.Tray.WinUI/Pages/LocalAiPage.xaml
@@ -12,9 +12,39 @@
Style="{StaticResource CaptionTextBlockStyle}"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" TextWrapping="Wrap"/>
-
+
+
+
+
+
+
+
+
+
-
+
+
-
+
+
@@ -86,11 +121,16 @@
Content="Retry setup or download" Click="OnRetrySetup" HorizontalAlignment="Left"
AutomationProperties.AutomationId="LocalAiRetrySetup"/>
-
+
+
-
+
+
@@ -113,7 +153,8 @@
AutomationProperties.AutomationId="LocalAiOpenChat"/>
-
+
+
diff --git a/src/OpenClaw.Tray.WinUI/Pages/LocalAiPage.xaml.cs b/src/OpenClaw.Tray.WinUI/Pages/LocalAiPage.xaml.cs
index a96c94bbc..363f46412 100644
--- a/src/OpenClaw.Tray.WinUI/Pages/LocalAiPage.xaml.cs
+++ b/src/OpenClaw.Tray.WinUI/Pages/LocalAiPage.xaml.cs
@@ -1,4 +1,5 @@
using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Automation;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;
using OpenClaw.Shared;
@@ -87,6 +88,13 @@ private void RefreshFromViewModel()
ActionErrorText.Visibility = string.IsNullOrWhiteSpace(ActionErrorText.Text) ? Visibility.Collapsed : Visibility.Visible;
EngineBusyIndicator.IsActive = _viewModel.IsBusy;
EngineBusyIndicator.Visibility = _viewModel.IsBusy ? Visibility.Visible : Visibility.Collapsed;
+ LocalAiUnavailableInfoBar.Visibility =
+ _viewModel.IsAvailabilityKnown && !_viewModel.IsLocalAiAvailable
+ ? Visibility.Visible
+ : Visibility.Collapsed;
+ SetOptionAvailability(LocalAiEngineOption, _viewModel.AreOptionsEnabled);
+ SetOptionAvailability(LocalAiModelOption, _viewModel.AreOptionsEnabled);
+ SetOptionAvailability(LocalAiGatewayOption, _viewModel.AreOptionsEnabled);
StartButton.IsEnabled = _viewModel.CanStart;
StopButton.IsEnabled = _viewModel.CanStop;
RestartButton.IsEnabled = _viewModel.CanRestart;
@@ -104,6 +112,21 @@ private void RefreshFromViewModel()
private void OnRetrySetup(object sender, RoutedEventArgs e) => _viewModel?.RetrySetup();
private void OnRepairConnection(object sender, RoutedEventArgs e) => _viewModel?.RepairConnection();
private void OnOpenChat(object sender, RoutedEventArgs e) => _viewModel?.OpenChat();
+ private void LocalAiUnavailableDetails_Click(object sender, RoutedEventArgs e)
+ {
+ LocalAiUnavailableReasonText.Text = _viewModel?.LocalAiUnavailableReason ?? string.Empty;
+ LocalAiUnavailableDetailsTip.IsOpen = !LocalAiUnavailableDetailsTip.IsOpen;
+ }
+
+ private static void SetOptionAvailability(Control option, bool isAvailable)
+ {
+ option.IsEnabled = isAvailable;
+ option.Opacity = isAvailable ? 1 : 0.55;
+ AutomationProperties.SetHelpText(
+ option,
+ isAvailable ? string.Empty : "Unavailable because Local AI is not available on this device.");
+ }
+
private static void RunAction(Func> action, string source) =>
AsyncEventHandlerGuard.Run(action, new AppLogger(), source);
private static Brush ResolveBrush(string key) => (Brush)Application.Current.Resources[key];
diff --git a/src/OpenClaw.Tray.WinUI/Presentation/AppServiceRegistration.cs b/src/OpenClaw.Tray.WinUI/Presentation/AppServiceRegistration.cs
index 24e656243..2d8a61746 100644
--- a/src/OpenClaw.Tray.WinUI/Presentation/AppServiceRegistration.cs
+++ b/src/OpenClaw.Tray.WinUI/Presentation/AppServiceRegistration.cs
@@ -1,4 +1,5 @@
using OpenClaw.Shared.ExecApprovals;
+using OpenClaw.Shared.Inference;
using Microsoft.Extensions.DependencyInjection;
using OpenClawTray.Chat;
using OpenClawTray.Services;
@@ -42,6 +43,7 @@ public static IServiceCollection AddOpenClawTrayCore(this IServiceCollection ser
// its Saved-event subscription during shutdown.
services.AddSingleton();
services.AddSingleton();
+ services.AddSingleton();
// Container-owned navigation lifetime manager (disposed with the root provider).
services.AddSingleton();
diff --git a/src/OpenClaw.Tray.WinUI/Presentation/LocalAiPageViewModel.cs b/src/OpenClaw.Tray.WinUI/Presentation/LocalAiPageViewModel.cs
index 0b8d7980a..e65289383 100644
--- a/src/OpenClaw.Tray.WinUI/Presentation/LocalAiPageViewModel.cs
+++ b/src/OpenClaw.Tray.WinUI/Presentation/LocalAiPageViewModel.cs
@@ -1,5 +1,6 @@
using OpenClaw.Connection;
using OpenClaw.Connection.LocalAi;
+using OpenClaw.Shared.Inference;
using OpenClaw.Shared.Inference.Catalog;
using OpenClawTray.Services;
using System.ComponentModel;
@@ -18,24 +19,31 @@ internal sealed class LocalAiPageViewModel : INavigationAware, IDisposable, INot
private readonly IPermissionsPageRuntimeSource _gatewaySource;
private readonly IAppCommands _appCommands;
private readonly IUiDispatcher _dispatcher;
+ private readonly IHostHardwareProbe _hardwareProbe;
private LocalAiRuntimeSnapshot _runtimeSnapshot;
private GatewayConnectionSnapshot _gatewaySnapshot;
private CancellationTokenSource? _refreshCancellation;
+ private CancellationTokenSource? _availabilityCancellation;
private bool _subscribed;
private bool _disposed;
private bool _isBusy;
private string? _actionError;
+ private bool _isAvailabilityKnown;
+ private bool _isLocalAiAvailable;
+ private string? _localAiUnavailableReason;
public LocalAiPageViewModel(
ILocalAiRuntime runtime,
IPermissionsPageRuntimeSource gatewaySource,
IAppCommands appCommands,
- IUiDispatcher dispatcher)
+ IUiDispatcher dispatcher,
+ IHostHardwareProbe hardwareProbe)
{
_runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
_gatewaySource = gatewaySource ?? throw new ArgumentNullException(nameof(gatewaySource));
_appCommands = appCommands ?? throw new ArgumentNullException(nameof(appCommands));
_dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
+ _hardwareProbe = hardwareProbe ?? throw new ArgumentNullException(nameof(hardwareProbe));
_runtimeSnapshot = runtime.Snapshot;
_gatewaySnapshot = gatewaySource.Current.ConnectionSnapshot;
}
@@ -110,18 +118,22 @@ public LocalAiPageViewModel(
public string? GatewayDetail => _gatewaySnapshot.GatewayName ?? _gatewaySnapshot.GatewayUrl;
public string? ActionError => _actionError;
public bool IsBusy => _isBusy;
- public bool CanStart => !IsBusy && HasManagedInstall &&
+ public bool IsAvailabilityKnown => _isAvailabilityKnown;
+ public bool IsLocalAiAvailable => _isAvailabilityKnown && _isLocalAiAvailable;
+ public bool AreOptionsEnabled => !_isAvailabilityKnown || _isLocalAiAvailable;
+ public string? LocalAiUnavailableReason => _localAiUnavailableReason;
+ public bool CanStart => AreOptionsEnabled && !IsBusy && HasManagedInstall &&
_runtimeSnapshot.State is LocalAiRuntimeState.Stopped or LocalAiRuntimeState.Failed;
- public bool CanStop => !IsBusy && _runtimeSnapshot.Ownership == LocalAiOwnership.CompanionManaged &&
+ public bool CanStop => AreOptionsEnabled && !IsBusy && _runtimeSnapshot.Ownership == LocalAiOwnership.CompanionManaged &&
_runtimeSnapshot.State is LocalAiRuntimeState.Starting or LocalAiRuntimeState.Healthy;
- public bool CanRestart => !IsBusy && _runtimeSnapshot.Ownership == LocalAiOwnership.CompanionManaged &&
+ public bool CanRestart => AreOptionsEnabled && !IsBusy && _runtimeSnapshot.Ownership == LocalAiOwnership.CompanionManaged &&
_runtimeSnapshot.State == LocalAiRuntimeState.Healthy;
- public bool CanOpenLogs => !IsBusy && HasManagedInstall;
- public bool CanRetrySetup => !IsBusy && ModelState is
+ public bool CanOpenLogs => AreOptionsEnabled && !IsBusy && HasManagedInstall;
+ public bool CanRetrySetup => AreOptionsEnabled && !IsBusy && ModelState is
LocalAiModelPresentationState.NotInstalled or LocalAiModelPresentationState.Unknown;
- public bool CanRepairConnection => !IsBusy && GatewayState is not
+ public bool CanRepairConnection => AreOptionsEnabled && !IsBusy && GatewayState is not
(LocalAiGatewayPresentationState.Connected or LocalAiGatewayPresentationState.Connecting);
- public bool CanOpenChat => !IsBusy &&
+ public bool CanOpenChat => AreOptionsEnabled && !IsBusy &&
GatewayState == LocalAiGatewayPresentationState.Connected &&
_runtimeSnapshot.State == LocalAiRuntimeState.Healthy &&
ModelState is LocalAiModelPresentationState.Verified or LocalAiModelPresentationState.Loaded;
@@ -144,11 +156,13 @@ public void Activate(object? parameter)
ApplyRuntimeSnapshot(_runtime.Snapshot);
ApplyGatewaySnapshot(_gatewaySource.Current.ConnectionSnapshot);
StartRuntimeRefresh();
+ StartAvailabilityRefresh();
}
public void Deactivate()
{
CancelRuntimeRefresh();
+ CancelAvailabilityRefresh();
if (_subscribed)
{
_runtime.StateChanged -= OnRuntimeStateChanged;
@@ -189,6 +203,67 @@ private void CancelRuntimeRefresh()
cancellation?.Cancel();
}
+ private void StartAvailabilityRefresh()
+ {
+ CancelAvailabilityRefresh();
+ var cancellation = new CancellationTokenSource();
+ _availabilityCancellation = cancellation;
+ _ = RefreshAvailabilityAsync(cancellation);
+ }
+
+ private void CancelAvailabilityRefresh()
+ {
+ CancellationTokenSource? cancellation = _availabilityCancellation;
+ _availabilityCancellation = null;
+ cancellation?.Cancel();
+ }
+
+ private async Task RefreshAvailabilityAsync(CancellationTokenSource cancellation)
+ {
+ try
+ {
+ HostHardwareInfo hardware = await Task.Run(
+ _hardwareProbe.Probe,
+ cancellation.Token).ConfigureAwait(false);
+ LocalInferenceEligibilityResult eligibility = LocalInferenceEligibility.Evaluate(
+ hardware,
+ _runtimeSnapshot.ModelId);
+ bool isAvailable = eligibility.CanInstall;
+ string? unavailableReason = isAvailable
+ ? null
+ : LocalInferenceEligibilityDiagnostics.DescribeUnavailable(eligibility);
+ ApplyOnUiThread(() =>
+ {
+ _isAvailabilityKnown = true;
+ _isLocalAiAvailable = isAvailable;
+ _localAiUnavailableReason = unavailableReason;
+ OnPropertyChanged(null);
+ });
+ }
+ catch (OperationCanceledException) when (cancellation.IsCancellationRequested)
+ {
+ }
+ catch
+ {
+ const string unavailableReason =
+ "OpenClaw could not read the NVIDIA GPU, driver, CUDA, or memory information. " +
+ "Check the NVIDIA driver installation and try setup again.";
+ ApplyOnUiThread(() =>
+ {
+ _isAvailabilityKnown = true;
+ _isLocalAiAvailable = false;
+ _localAiUnavailableReason = unavailableReason;
+ OnPropertyChanged(null);
+ });
+ }
+ finally
+ {
+ if (ReferenceEquals(_availabilityCancellation, cancellation))
+ _availabilityCancellation = null;
+ cancellation.Dispose();
+ }
+ }
+
private async Task RefreshRuntimeSnapshotAsync(CancellationTokenSource cancellation)
{
try
diff --git a/src/OpenClaw.Tray.WinUI/Strings/en-us/Resources.resw b/src/OpenClaw.Tray.WinUI/Strings/en-us/Resources.resw
index eba280464..4c299e397 100644
--- a/src/OpenClaw.Tray.WinUI/Strings/en-us/Resources.resw
+++ b/src/OpenClaw.Tray.WinUI/Strings/en-us/Resources.resw
@@ -7042,6 +7042,9 @@ Make sure the gateway is running.
Local AI
Local AI
Run the verified native llama-server privately on this computer. Models load only when requested.
+ Local AI is not available
+ See why
+ Why Local AI is unavailable
llama-server
Version
Endpoint
diff --git a/src/OpenClaw.Tray.WinUI/Strings/fr-fr/Resources.resw b/src/OpenClaw.Tray.WinUI/Strings/fr-fr/Resources.resw
index 6ad28bcf7..6f5f3242c 100644
--- a/src/OpenClaw.Tray.WinUI/Strings/fr-fr/Resources.resw
+++ b/src/OpenClaw.Tray.WinUI/Strings/fr-fr/Resources.resw
@@ -6978,6 +6978,9 @@ Le binaire wxc-exec est introuvable. {1} S'il s'agit d'une build développeur, c
IA locale
IA locale
Exécutez le llama-server natif vérifié en privé sur cet ordinateur. Les modèles ne sont chargés qu'à la demande.
+ L'IA locale n'est pas disponible
+ Voir pourquoi
+ Pourquoi l'IA locale n'est pas disponible
llama-server
Version du moteur
Point de terminaison
diff --git a/src/OpenClaw.Tray.WinUI/Strings/nl-nl/Resources.resw b/src/OpenClaw.Tray.WinUI/Strings/nl-nl/Resources.resw
index a7763f031..03e267a4e 100644
--- a/src/OpenClaw.Tray.WinUI/Strings/nl-nl/Resources.resw
+++ b/src/OpenClaw.Tray.WinUI/Strings/nl-nl/Resources.resw
@@ -6979,6 +6979,9 @@ Het binaire bestand wxc-exec is niet gevonden. {1} Als dit een ontwikkelaarsbuil
Lokale AI
Lokale AI
Voer de geverifieerde native llama-server privé uit op deze computer. Modellen worden alleen op aanvraag geladen.
+ Lokale AI is niet beschikbaar
+ Bekijk waarom
+ Waarom lokale AI niet beschikbaar is
llama-server
Versie
Eindpunt
diff --git a/src/OpenClaw.Tray.WinUI/Strings/zh-cn/Resources.resw b/src/OpenClaw.Tray.WinUI/Strings/zh-cn/Resources.resw
index 195902787..522c46afb 100644
--- a/src/OpenClaw.Tray.WinUI/Strings/zh-cn/Resources.resw
+++ b/src/OpenClaw.Tray.WinUI/Strings/zh-cn/Resources.resw
@@ -6978,6 +6978,9 @@
本地 AI
本地 AI
在此计算机上私密运行经过验证的原生 llama-server。模型仅在收到请求时加载。
+ 本地 AI 不可用
+ 查看原因
+ 本地 AI 不可用的原因
llama-server
版本
终结点
diff --git a/src/OpenClaw.Tray.WinUI/Strings/zh-tw/Resources.resw b/src/OpenClaw.Tray.WinUI/Strings/zh-tw/Resources.resw
index c1fb6f93c..795ec24a7 100644
--- a/src/OpenClaw.Tray.WinUI/Strings/zh-tw/Resources.resw
+++ b/src/OpenClaw.Tray.WinUI/Strings/zh-tw/Resources.resw
@@ -6978,6 +6978,9 @@
本機 AI
本機 AI
在這部電腦上私密執行經過驗證的原生 llama-server。模型只會在收到要求時載入。
+ 本機 AI 無法使用
+ 查看原因
+ 本機 AI 無法使用的原因
llama-server
版本
端點
diff --git a/tests/OpenClaw.Tray.Tests/AppRefactorContractTests.cs b/tests/OpenClaw.Tray.Tests/AppRefactorContractTests.cs
index da3eea845..dcbe1ab45 100644
--- a/tests/OpenClaw.Tray.Tests/AppRefactorContractTests.cs
+++ b/tests/OpenClaw.Tray.Tests/AppRefactorContractTests.cs
@@ -1361,12 +1361,14 @@ 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.Contains("One or more Local AI requirements are unavailable.", xaml);
- Assert.Matches(
- new Regex(
- "]*>\\s*" +
- ""),
- xaml);
+ Assert.Contains("This PC does not meet one or more Local AI requirements.", xaml);
+ Assert.Contains("Title=\"Local AI is not available\"", xaml);
+ Assert.Contains("SetLocalAiOptionAvailability(isAvailable: false)", ExtractMethod(source, "ShowLocalAiUnavailable"));
+ Assert.Contains("Message=\"This PC does not meet one or more Local AI requirements.\"", xaml);
+ Assert.Contains("", xaml);
+ Assert.True(
+ xaml.IndexOf("x:Name=\"LocalAiUnavailablePanel\"", StringComparison.Ordinal) <
+ xaml.IndexOf("x:Name=\"LocalAiInstallReviewCard\"", StringComparison.Ordinal));
}
[Fact]
@@ -1374,11 +1376,12 @@ public void CapabilitiesPage_FiltersModelsBySelectedGpuCapacityAndShowsMemoryEvi
{
var root = TestRepositoryPaths.GetRepositoryRoot();
var source = File.ReadAllText(Path.Combine(root, "src", "OpenClaw.SetupEngine.UI", "Pages", "CapabilitiesPage.xaml.cs"));
+ var diagnostics = File.ReadAllText(Path.Combine(root, "src", "OpenClaw.Shared", "Inference", "Catalog", "LocalInferenceEligibilityDiagnostics.cs"));
Assert.Contains("LocalInferenceEligibility.GetRequiredMemoryBytes(model) <= capacityBytes", source);
- Assert.Contains("eligibility.RequiredTotalMemoryBytes", source);
- Assert.Contains("eligibility.DetectedTotalMemoryBytes", source);
- Assert.Contains("model weights, KV cache, and runtime workspace", source);
+ Assert.Contains("eligibility.RequiredTotalMemoryBytes", diagnostics);
+ Assert.Contains("eligibility.DetectedTotalMemoryBytes", diagnostics);
+ Assert.Contains("model weights, KV cache, and runtime workspace", diagnostics);
Assert.DoesNotContain("2 GiB runtime margin", source);
Assert.DoesNotContain("HardwareProfile", source);
Assert.DoesNotContain("RTX PRO 6000", source);
diff --git a/tests/OpenClaw.Tray.Tests/LocalAiSetupUxContractTests.cs b/tests/OpenClaw.Tray.Tests/LocalAiSetupUxContractTests.cs
new file mode 100644
index 000000000..4e8b0ba99
--- /dev/null
+++ b/tests/OpenClaw.Tray.Tests/LocalAiSetupUxContractTests.cs
@@ -0,0 +1,130 @@
+using OpenClaw.TestSupport;
+
+namespace OpenClaw.Tray.Tests;
+
+public sealed class LocalAiSetupUxContractTests
+{
+ [Fact]
+ public void WelcomePage_ShowsLocalAiAsIconBadgeBesideRecommended()
+ {
+ string root = TestRepositoryPaths.GetRepositoryRoot();
+ string xaml = File.ReadAllText(Path.Combine(
+ root,
+ "src",
+ "OpenClaw.SetupEngine.UI",
+ "Pages",
+ "WelcomePage.xaml"));
+ string source = File.ReadAllText(Path.Combine(
+ root,
+ "src",
+ "OpenClaw.SetupEngine.UI",
+ "Pages",
+ "WelcomePage.xaml.cs"));
+ string badge = ExtractElement(xaml, "LocalAiAvailabilityBadge", "");
+
+ Assert.Contains("WelcomeLocalAiAvailable", badge);
+ Assert.Contains("Glyph=\"\"", badge);
+ Assert.Contains("Local AI available", badge);
+ Assert.Contains("AutomationProperties.AccessibilityView=\"Raw\"", badge);
+ AssertInOrder(xaml, "Text=\"Recommended\"", "x:Name=\"LocalAiAvailabilityBadge\"");
+ Assert.DoesNotContain("LocalAiAvailabilityPanel", xaml);
+ Assert.DoesNotContain("LocalAiAvailabilityText", xaml);
+ Assert.DoesNotContain("detected. Install a local gateway", source);
+ Assert.Contains("AutomationProperties.SetName(", source);
+ }
+
+ [Fact]
+ public void CapabilitiesReview_SeparatesReasonActionFromDisabledOptions()
+ {
+ string root = TestRepositoryPaths.GetRepositoryRoot();
+ string xaml = File.ReadAllText(Path.Combine(
+ root,
+ "src",
+ "OpenClaw.SetupEngine.UI",
+ "Pages",
+ "CapabilitiesPage.xaml"));
+ string source = File.ReadAllText(Path.Combine(
+ root,
+ "src",
+ "OpenClaw.SetupEngine.UI",
+ "Pages",
+ "CapabilitiesPage.xaml.cs"));
+ string infoBar = ExtractElement(xaml, "LocalAiUnavailablePanel", "");
+
+ Assert.Contains("Title=\"Local AI is not available\"", xaml);
+ Assert.Contains("Severity=\"Informational\"", xaml);
+ Assert.Contains("AutomationProperties.LiveSetting=\"Polite\"", xaml);
+ Assert.Contains("Message=\"This PC does not meet one or more Local AI requirements.\"", infoBar);
+ Assert.Contains("", infoBar);
+ Assert.DoesNotContain("= 0, $"Could not find {elementName}.");
+ int end = source.IndexOf(closingTag, start, StringComparison.Ordinal);
+ Assert.True(end >= 0, $"Could not find the end of {elementName}.");
+ return source[start..(end + closingTag.Length)];
+ }
+
+ private static void AssertInOrder(string source, params string[] values)
+ {
+ int previous = -1;
+ foreach (string value in values)
+ {
+ int current = source.IndexOf(value, previous + 1, StringComparison.Ordinal);
+ Assert.True(current > previous, $"Expected '{value}' after the previous value.");
+ previous = current;
+ }
+ }
+}
diff --git a/tests/OpenClaw.Tray.Tests/Presentation/AppServiceRegistrationTests.cs b/tests/OpenClaw.Tray.Tests/Presentation/AppServiceRegistrationTests.cs
index f83e5f57e..c9f0a4f49 100644
--- a/tests/OpenClaw.Tray.Tests/Presentation/AppServiceRegistrationTests.cs
+++ b/tests/OpenClaw.Tray.Tests/Presentation/AppServiceRegistrationTests.cs
@@ -2,6 +2,7 @@
using Microsoft.Extensions.DependencyInjection;
using OpenClaw.Shared;
using OpenClaw.Shared.ExecApprovals;
+using OpenClaw.Shared.Inference;
using OpenClawTray.Chat;
using OpenClawTray.Presentation;
using OpenClawTray.Services;
@@ -68,6 +69,10 @@ public void AppOwnedSingletons_ResolveToTheProvidedInstances()
Assert.Same(settings, provider.GetRequiredService());
Assert.Same(execApprovalsStore, provider.GetRequiredService());
Assert.Same(runtimeHost, provider.GetRequiredService());
+ Assert.IsType(provider.GetRequiredService());
+ Assert.Same(
+ provider.GetRequiredService(),
+ provider.GetRequiredService());
Assert.Same(provider.GetRequiredService(), provider.GetRequiredService());
Assert.Same(provider.GetRequiredService(), provider.GetRequiredService());
}
diff --git a/tests/OpenClaw.Tray.Tests/Presentation/LocalAiPageViewModelTests.cs b/tests/OpenClaw.Tray.Tests/Presentation/LocalAiPageViewModelTests.cs
new file mode 100644
index 000000000..ceb45ebfc
--- /dev/null
+++ b/tests/OpenClaw.Tray.Tests/Presentation/LocalAiPageViewModelTests.cs
@@ -0,0 +1,174 @@
+using OpenClaw.Connection;
+using OpenClaw.Connection.LocalAi;
+using OpenClaw.Shared.Inference;
+using OpenClaw.Shared.Inference.Catalog;
+using OpenClawTray.Presentation;
+using System.Runtime.InteropServices;
+
+namespace OpenClaw.Tray.Tests.Presentation;
+
+public sealed class LocalAiPageViewModelTests
+{
+ [Fact]
+ public async Task UnsupportedHardware_DisablesEveryOptionAndCommand()
+ {
+ var runtime = new FakeLocalAiRuntime(CreateInstalledSnapshot());
+ var runtimeHost = new FakePermissionsPageRuntimeHost
+ {
+ ConnectionSnapshot = GatewayConnectionSnapshot.Idle with
+ {
+ OperatorState = RoleConnectionState.Idle,
+ },
+ };
+ using var gatewaySource = new PermissionsPageRuntimeSource(runtimeHost);
+ var commands = new FakeAppCommands();
+ using var viewModel = new LocalAiPageViewModel(
+ runtime,
+ gatewaySource,
+ commands,
+ new RecordingUiDispatcher(),
+ new FixedHardwareProbe(HostHardwareInfo.Unknown));
+
+ Assert.False(viewModel.IsAvailabilityKnown);
+ Assert.True(viewModel.AreOptionsEnabled);
+
+ await ActivateAndWaitForAvailabilityAsync(viewModel);
+
+ Assert.True(viewModel.IsAvailabilityKnown);
+ Assert.False(viewModel.IsLocalAiAvailable);
+ Assert.False(viewModel.AreOptionsEnabled);
+ Assert.Contains("NVIDIA GPU", viewModel.LocalAiUnavailableReason);
+ Assert.False(viewModel.CanStart);
+ Assert.False(viewModel.CanStop);
+ Assert.False(viewModel.CanRestart);
+ Assert.False(viewModel.CanOpenLogs);
+ Assert.False(viewModel.CanRetrySetup);
+ Assert.False(viewModel.CanRepairConnection);
+ Assert.False(viewModel.CanOpenChat);
+ Assert.False(viewModel.OpenLogs());
+ Assert.False(viewModel.RetrySetup());
+ Assert.False(viewModel.RepairConnection());
+ Assert.False(viewModel.OpenChat());
+ Assert.Equal(0, commands.OpenLocalAiLogsCount);
+ Assert.Equal(0, commands.ShowOnboardingCount);
+ Assert.Equal(0, commands.ReconnectCount);
+ Assert.Equal(0, commands.ShowChatCount);
+ }
+
+ [Fact]
+ public async Task QualifiedHardware_EnablesApplicableOptionsAndRoutesActions()
+ {
+ var runtime = new FakeLocalAiRuntime(CreateInstalledSnapshot());
+ var runtimeHost = new FakePermissionsPageRuntimeHost
+ {
+ ConnectionSnapshot = GatewayConnectionSnapshot.Idle with
+ {
+ OperatorState = RoleConnectionState.Connected,
+ },
+ };
+ using var gatewaySource = new PermissionsPageRuntimeSource(runtimeHost);
+ var commands = new FakeAppCommands();
+ using var viewModel = new LocalAiPageViewModel(
+ runtime,
+ gatewaySource,
+ commands,
+ new RecordingUiDispatcher(),
+ new FixedHardwareProbe(CreateQualifiedHardware()));
+
+ await ActivateAndWaitForAvailabilityAsync(viewModel);
+
+ Assert.True(viewModel.IsLocalAiAvailable);
+ Assert.True(viewModel.AreOptionsEnabled);
+ Assert.Null(viewModel.LocalAiUnavailableReason);
+ Assert.True(viewModel.CanStop);
+ Assert.True(viewModel.CanRestart);
+ Assert.True(viewModel.CanOpenLogs);
+ Assert.True(viewModel.CanOpenChat);
+ Assert.True(viewModel.OpenLogs());
+ Assert.True(viewModel.OpenChat());
+ Assert.Equal(1, commands.OpenLocalAiLogsCount);
+ Assert.Equal(1, commands.ShowChatCount);
+ }
+
+ private static async Task ActivateAndWaitForAvailabilityAsync(LocalAiPageViewModel viewModel)
+ {
+ var completion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+ viewModel.PropertyChanged += (_, _) =>
+ {
+ if (viewModel.IsAvailabilityKnown)
+ completion.TrySetResult();
+ };
+
+ viewModel.Activate(null);
+ if (!viewModel.IsAvailabilityKnown)
+ await completion.Task.WaitAsync(TimeSpan.FromSeconds(5));
+ }
+
+ private static HostHardwareInfo CreateQualifiedHardware() =>
+ new(
+ Architecture.X64,
+ TotalPhysicalMemoryBytes: 256_000_000_000,
+ AvailablePhysicalMemoryBytes: 128_000_000_000,
+ Gpus:
+ [
+ new GpuInfo(
+ GpuVendor.Nvidia,
+ "NVIDIA Test GPU",
+ GpuVisibleMemoryBytes: 128_000_000_000,
+ FreeGpuVisibleMemoryBytes: 128_000_000_000,
+ DriverVersion: "620.0",
+ CudaMajorVersion: 13,
+ StableId: "GPU-test"),
+ ],
+ VulkanAvailable: false);
+
+ private static LocalAiRuntimeSnapshot CreateInstalledSnapshot()
+ {
+ DateTimeOffset now = DateTimeOffset.UtcNow;
+ string modelId = LocalModelCatalog.Models[0].Id;
+ return new LocalAiRuntimeSnapshot(
+ LocalAiRuntimeState.Healthy,
+ LocalAiOwnership.CompanionManaged,
+ new Uri("http://127.0.0.1:18080"),
+ "test",
+ modelId,
+ new LocalAiModelEvidence(
+ LocalAiModelAvailabilityState.Verified,
+ now,
+ new string('0', 64),
+ sizeBytes: 1),
+ ProcessId: 1234,
+ ProcessStartedAtUtc: now,
+ Detail: null,
+ UpdatedAtUtc: now);
+ }
+
+ private sealed class FixedHardwareProbe(HostHardwareInfo hardware) : IHostHardwareProbe
+ {
+ public HostHardwareInfo Probe() => hardware;
+ }
+
+ private sealed class FakeLocalAiRuntime(LocalAiRuntimeSnapshot snapshot) : ILocalAiRuntime
+ {
+ public LocalAiRuntimeSnapshot Snapshot { get; private set; } = snapshot;
+ public event EventHandler? StateChanged
+ {
+ add { }
+ remove { }
+ }
+
+ public Task EnsureStartedAsync(CancellationToken cancellationToken = default) =>
+ Task.FromResult(Snapshot);
+
+ public Task StopAsync(CancellationToken cancellationToken = default) =>
+ Task.FromResult(Snapshot);
+
+ public Task RestartAsync(CancellationToken cancellationToken = default) =>
+ Task.FromResult(Snapshot);
+
+ public Task RefreshAsync(CancellationToken cancellationToken = default) =>
+ Task.FromResult(Snapshot);
+
+ public ValueTask DisposeAsync() => ValueTask.CompletedTask;
+ }
+}
diff --git a/tests/OpenClaw.Tray.Tests/Presentation/PresentationTestDoubles.cs b/tests/OpenClaw.Tray.Tests/Presentation/PresentationTestDoubles.cs
index b11f0055a..c54142325 100644
--- a/tests/OpenClaw.Tray.Tests/Presentation/PresentationTestDoubles.cs
+++ b/tests/OpenClaw.Tray.Tests/Presentation/PresentationTestDoubles.cs
@@ -64,17 +64,22 @@ internal sealed class FakeAppCommands : IAppCommands, IDisposable
public List Navigations { get; } = new();
public IReadOnlyList OperationLog => _operationLog;
public bool Disposed { get; private set; }
+ public int ReconnectCount { get; private set; }
+ public int ShowChatCount { get; private set; }
+ public int ShowOnboardingCount { get; private set; }
+ public int OpenLocalAiLogsCount { get; private set; }
public void ClearOperationLog() => _operationLog.Clear();
public void OpenDashboard(string? path = null) { }
public void Navigate(string pageTag) => Navigations.Add(pageTag);
- public void Reconnect() { }
+ public void Reconnect() => ReconnectCount++;
public void Disconnect() { }
public void ShowVoiceOverlay() { }
- public void ShowChat() { }
+ public void ShowChat() => ShowChatCount++;
public void CheckForUpdates() { }
- public void ShowOnboarding() { }
+ public void ShowOnboarding() => ShowOnboardingCount++;
+ public void OpenLocalAiLogs() => OpenLocalAiLogsCount++;
public void ShowGatewayWizard() { }
public void ShowConnectionStatus() { }
public void NotifySettingsSaved()