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