diff --git a/README.md b/README.md
index d728728..f85d7b7 100644
--- a/README.md
+++ b/README.md
@@ -22,7 +22,7 @@ This monorepo contains three projects:
### Prerequisites
- Windows 10 (20H2+) or Windows 11
-- .NET 10.0 SDK (preview) - https://dotnet.microsoft.com/download/dotnet/10.0
+- .NET 10.0 SDK - https://dotnet.microsoft.com/download/dotnet/10.0
- Windows 10 SDK (for WinUI build) - install via Visual Studio or standalone
- WebView2 Runtime - pre-installed on modern Windows, or get from https://developer.microsoft.com/microsoft-edge/webview2
- PowerToys (optional, for Command Palette extension)
@@ -212,6 +212,8 @@ When Node Mode is enabled in Settings, your Windows PC becomes a **node** that t
> 🔒 **Exec Policy**: `system.run` is gated by an approval policy (saved to `exec-policy.json`). Default rules allow read-only commands (echo, Get-*, hostname, etc.) and deny destructive operations (rm, shutdown, registry edits). Use `system.execApprovals.get/set` to view/modify rules remotely.
+ > 🔐 **Web Chat secure context**: Remote web chat requires `https://` (or localhost). If using a self-signed cert, trust it in Windows (Trusted Root Certification Authorities) or use an SSH tunnel to localhost.
+
#### Node Status in Tray Menu
The tray menu shows node connection status:
diff --git a/src/OpenClaw.CommandPalette/Pages/OpenClawPage.cs b/src/OpenClaw.CommandPalette/Pages/OpenClawPage.cs
index 7b4af09..f0f7f07 100644
--- a/src/OpenClaw.CommandPalette/Pages/OpenClawPage.cs
+++ b/src/OpenClaw.CommandPalette/Pages/OpenClawPage.cs
@@ -19,7 +19,7 @@ internal sealed partial class OpenClawPage : ListPage
public override IListItem[] GetItems()
{
return [
- new ListItem(new OpenUrlCommand("http://localhost:18789"))
+ new ListItem(new OpenUrlCommand("openclaw://dashboard"))
{
Title = "🦞 Open Dashboard",
Subtitle = "Open OpenClaw web dashboard"
diff --git a/src/OpenClaw.Tray.WinUI/App.xaml.cs b/src/OpenClaw.Tray.WinUI/App.xaml.cs
index 03a4e53..a062ead 100644
--- a/src/OpenClaw.Tray.WinUI/App.xaml.cs
+++ b/src/OpenClaw.Tray.WinUI/App.xaml.cs
@@ -514,7 +514,7 @@ public partial class App : Application
case "webchat": ShowWebChat(); break;
case "quicksend": ShowQuickSend(); break;
case "history": ShowNotificationHistory(); break;
- case "healthcheck": _ = RunHealthCheckAsync(); break;
+ case "healthcheck": _ = RunHealthCheckAsync(userInitiated: true); break;
case "settings": ShowSettings(); break;
case "autostart": ToggleAutoStart(); break;
case "log": OpenLogFile(); break;
@@ -801,7 +801,7 @@ public partial class App : Application
flyout.Items.Add(historyItem);
var healthCheckItem = new MenuFlyoutItem { Text = "🔄 Run Health Check" };
- healthCheckItem.Click += async (s, e) => await RunHealthCheckAsync();
+ healthCheckItem.Click += async (s, e) => await RunHealthCheckAsync(userInitiated: true);
flyout.Items.Add(healthCheckItem);
flyout.Items.Add(new MenuFlyoutSeparator());
@@ -1106,18 +1106,42 @@ public partial class App : Application
_ = RunHealthCheckAsync();
}
- private async Task RunHealthCheckAsync()
+ private async Task RunHealthCheckAsync(bool userInitiated = false)
{
- if (_gatewayClient == null) return;
+ if (_gatewayClient == null)
+ {
+ if (userInitiated)
+ {
+ new ToastContentBuilder()
+ .AddText("Health Check")
+ .AddText("Gateway is not connected yet.")
+ .Show();
+ }
+ return;
+ }
try
{
_lastCheckTime = DateTime.Now;
await _gatewayClient.CheckHealthAsync();
+ if (userInitiated)
+ {
+ new ToastContentBuilder()
+ .AddText("Health Check")
+ .AddText("Health check request sent.")
+ .Show();
+ }
}
catch (Exception ex)
{
Logger.Warn($"Health check failed: {ex.Message}");
+ if (userInitiated)
+ {
+ new ToastContentBuilder()
+ .AddText("Health Check Failed")
+ .AddText(ex.Message)
+ .Show();
+ }
}
}
@@ -1282,11 +1306,18 @@ public partial class App : Application
var baseUrl = _settings.GatewayUrl
.Replace("ws://", "http://")
- .Replace("wss://", "https://");
-
- var url = string.IsNullOrEmpty(path)
- ? $"{baseUrl}?token={Uri.EscapeDataString(_settings.Token)}"
- : $"{baseUrl}/{path}?token={Uri.EscapeDataString(_settings.Token)}";
+ .Replace("wss://", "https://")
+ .TrimEnd('/');
+
+ var url = string.IsNullOrEmpty(path)
+ ? baseUrl
+ : $"{baseUrl}/{path.TrimStart('/')}";
+
+ if (!string.IsNullOrEmpty(_settings.Token))
+ {
+ var separator = url.Contains('?') ? "&" : "?";
+ url = $"{url}{separator}token={Uri.EscapeDataString(_settings.Token)}";
+ }
try
{
diff --git a/src/OpenClaw.Tray.WinUI/Dialogs/QuickSendDialog.cs b/src/OpenClaw.Tray.WinUI/Dialogs/QuickSendDialog.cs
index 8c50030..c6df342 100644
--- a/src/OpenClaw.Tray.WinUI/Dialogs/QuickSendDialog.cs
+++ b/src/OpenClaw.Tray.WinUI/Dialogs/QuickSendDialog.cs
@@ -1,3 +1,4 @@
+using Microsoft.Toolkit.Uwp.Notifications;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Input;
@@ -124,6 +125,10 @@ public sealed class QuickSendDialog : WindowEx
{
await _client.SendChatMessageAsync(message);
Logger.Info($"Quick send: {message}");
+ new ToastContentBuilder()
+ .AddText("Message Sent")
+ .AddText("Your message was sent to OpenClaw.")
+ .Show();
Close();
}
catch (Exception ex)
diff --git a/src/OpenClaw.Tray.WinUI/OpenClaw.Tray.WinUI.csproj b/src/OpenClaw.Tray.WinUI/OpenClaw.Tray.WinUI.csproj
index c81115a..ff4db7a 100644
--- a/src/OpenClaw.Tray.WinUI/OpenClaw.Tray.WinUI.csproj
+++ b/src/OpenClaw.Tray.WinUI/OpenClaw.Tray.WinUI.csproj
@@ -9,7 +9,7 @@
true
Assets\openclaw.ico
OpenClawTray
- 0.4.1
+ 0.4.2
diff --git a/src/OpenClaw.Tray.WinUI/Package.appxmanifest b/src/OpenClaw.Tray.WinUI/Package.appxmanifest
index 3a4b739..068747b 100644
--- a/src/OpenClaw.Tray.WinUI/Package.appxmanifest
+++ b/src/OpenClaw.Tray.WinUI/Package.appxmanifest
@@ -12,7 +12,7 @@
+ Version="0.4.2.0" />
OpenClaw Tray
diff --git a/src/OpenClaw.Tray.WinUI/Windows/WebChatWindow.xaml b/src/OpenClaw.Tray.WinUI/Windows/WebChatWindow.xaml
index 9756d8a..9e2e5a7 100644
--- a/src/OpenClaw.Tray.WinUI/Windows/WebChatWindow.xaml
+++ b/src/OpenClaw.Tray.WinUI/Windows/WebChatWindow.xaml
@@ -52,7 +52,7 @@
-
diff --git a/src/OpenClaw.Tray.WinUI/Windows/WebChatWindow.xaml.cs b/src/OpenClaw.Tray.WinUI/Windows/WebChatWindow.xaml.cs
index 3f8bfd2..478023c 100644
--- a/src/OpenClaw.Tray.WinUI/Windows/WebChatWindow.xaml.cs
+++ b/src/OpenClaw.Tray.WinUI/Windows/WebChatWindow.xaml.cs
@@ -99,14 +99,25 @@ public sealed partial class WebChatWindow : WindowEx
e.WebErrorStatus == CoreWebView2WebErrorStatus.ServerUnreachable))
{
Logger.Info("WebChatWindow: Gateway unreachable, showing friendly error");
- WebView.Visibility = Visibility.Collapsed;
- ErrorPanel.Visibility = Visibility.Visible;
- ErrorText.Text = "Can't reach OpenClaw Gateway\n\n" +
+ ShowErrorMessage("Can't reach OpenClaw Gateway\n\n" +
$"The gateway at {_gatewayUrl} is not responding.\n\n" +
"To connect:\n" +
"• Make sure your OpenClaw gateway is running\n" +
"• If remote, connect via VPN to your home network\n" +
- "• Or use SSH tunnel: ssh -N -L 18789:localhost:18789 your-server";
+ "• Or use SSH tunnel: ssh -N -L 18789:localhost:18789 your-server");
+ return;
+ }
+
+ if (!e.IsSuccess &&
+ e.WebErrorStatus.ToString().Contains("Certificate", StringComparison.OrdinalIgnoreCase))
+ {
+ Logger.Info("WebChatWindow: TLS certificate issue detected");
+ ShowErrorMessage(
+ "The gateway HTTPS certificate is not trusted.\n\n" +
+ "To connect securely:\n" +
+ "• Use an HTTPS gateway URL (for example: https://host.tailnet.ts.net)\n" +
+ "• If self-signed, import the cert into Windows Trusted Root Certification Authorities\n" +
+ "• Or use SSH tunnel to localhost and keep using localhost URLs");
}
};
WebView.CoreWebView2.NavigationCompleted += _navigationCompletedHandler;
@@ -159,6 +170,59 @@ public sealed partial class WebChatWindow : WindowEx
// Set to a test URL to bypass gateway (e.g., "https://www.bing.com"), or null for normal operation
private const string? DEBUG_TEST_URL = null;
+
+ private static bool IsLocalHost(Uri uri)
+ {
+ return uri.IsLoopback || string.Equals(uri.Host, "localhost", StringComparison.OrdinalIgnoreCase);
+ }
+
+ private bool TryBuildChatUrl(out string url, out string errorMessage)
+ {
+ url = string.Empty;
+ errorMessage = string.Empty;
+
+ if (!GatewayUrlHelper.TryNormalizeWebSocketUrl(_gatewayUrl, out var normalizedGatewayUrl) ||
+ !Uri.TryCreate(normalizedGatewayUrl, UriKind.Absolute, out var gatewayUri))
+ {
+ errorMessage = $"Invalid gateway URL: {_gatewayUrl}";
+ return false;
+ }
+
+ var webScheme = gatewayUri.Scheme.Equals("wss", StringComparison.OrdinalIgnoreCase)
+ ? "https"
+ : "http";
+
+ if (webScheme == "http" && !IsLocalHost(gatewayUri))
+ {
+ errorMessage =
+ "Web chat requires a secure context.\n\n" +
+ "There is no safe bypass for remote plain HTTP: browsers and WebView enforce this.\n\n" +
+ "Use one of these options:\n" +
+ "• Use a trusted HTTPS/WSS endpoint (Let's Encrypt, Tailscale Serve, Caddy)\n" +
+ "• If self-signed, import your gateway CA/cert into Windows Trusted Root (certmgr.msc)\n" +
+ "• Or tunnel to localhost: ssh -N -L 18789:localhost:18789 ";
+ return false;
+ }
+
+ var builder = new UriBuilder(gatewayUri)
+ {
+ Scheme = webScheme,
+ Port = gatewayUri.Port
+ };
+
+ var baseUrl = builder.Uri.GetLeftPart(UriPartial.Authority);
+ url = $"{baseUrl}?token={Uri.EscapeDataString(_token)}";
+ return true;
+ }
+
+ private void ShowErrorMessage(string message)
+ {
+ LoadingRing.IsActive = false;
+ LoadingRing.Visibility = Visibility.Collapsed;
+ WebView.Visibility = Visibility.Collapsed;
+ ErrorPanel.Visibility = Visibility.Visible;
+ ErrorText.Text = message;
+ }
private void NavigateToChat()
{
@@ -172,12 +236,15 @@ public sealed partial class WebChatWindow : WindowEx
return;
}
- var baseUrl = _gatewayUrl
- .Replace("ws://", "http://")
- .Replace("wss://", "https://");
-
- var url = $"{baseUrl}?token={Uri.EscapeDataString(_token)}";
- Logger.Info($"WebChatWindow: Navigating to {baseUrl} (token hidden)");
+ if (!TryBuildChatUrl(out var url, out var errorMessage))
+ {
+ Logger.Warn($"WebChatWindow: {errorMessage}");
+ ShowErrorMessage(errorMessage);
+ return;
+ }
+
+ var safeBaseUrl = url.Split('?')[0];
+ Logger.Info($"WebChatWindow: Navigating to {safeBaseUrl} (token hidden)");
WebView.CoreWebView2.Navigate(url);
}
@@ -193,10 +260,12 @@ public sealed partial class WebChatWindow : WindowEx
private void OnPopout(object sender, RoutedEventArgs e)
{
- var baseUrl = _gatewayUrl
- .Replace("ws://", "http://")
- .Replace("wss://", "https://");
- var url = $"{baseUrl}?token={Uri.EscapeDataString(_token)}";
+ if (!TryBuildChatUrl(out var url, out var errorMessage))
+ {
+ Logger.Warn($"WebChatWindow: {errorMessage}");
+ ShowErrorMessage(errorMessage);
+ return;
+ }
try
{
diff --git a/src/OpenClaw.Tray/OpenClaw.Tray.csproj b/src/OpenClaw.Tray/OpenClaw.Tray.csproj
index 7637904..b547b37 100644
--- a/src/OpenClaw.Tray/OpenClaw.Tray.csproj
+++ b/src/OpenClaw.Tray/OpenClaw.Tray.csproj
@@ -14,7 +14,7 @@
Scott Hanselman
OpenClaw Tray
Copyright © 2026 Scott Hanselman
- 0.4.1
+ 0.4.2
true
true