Improve web chat security UX and bump to 0.4.2
Some checks failed
Build and Test / test (push) Has been cancelled
Build and Test / build (win-arm64) (push) Has been cancelled
Build and Test / build (win-x64) (push) Has been cancelled
Build and Test / build-msix (ARM64, win-arm64) (push) Has been cancelled
Build and Test / build-msix (x64, win-x64) (push) Has been cancelled
Build and Test / build-extension (arm64) (push) Has been cancelled
Build and Test / build-extension (x64) (push) Has been cancelled
Build and Test / release (push) Has been cancelled

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Scott Hanselman 2026-02-16 21:49:13 -08:00
parent c5e5a291e9
commit 4d8d43bcfe
9 changed files with 136 additions and 29 deletions

View File

@ -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:

View File

@ -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"

View File

@ -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
{

View File

@ -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)

View File

@ -9,7 +9,7 @@
<EnableMsixTooling>true</EnableMsixTooling>
<ApplicationIcon>Assets\openclaw.ico</ApplicationIcon>
<RootNamespace>OpenClawTray</RootNamespace>
<Version>0.4.1</Version>
<Version>0.4.2</Version>
</PropertyGroup>
<!-- Unpackaged (default): traditional EXE distribution via Inno Setup -->

View File

@ -12,7 +12,7 @@
<Identity
Name="OpenClaw.Tray"
Publisher="CN=Scott Hanselman, O=Scott Hanselman, L=Forest Grove, S=Oregon, C=US"
Version="0.4.1.0" />
Version="0.4.2.0" />
<Properties>
<DisplayName>OpenClaw Tray</DisplayName>

View File

@ -52,7 +52,7 @@
<!-- Error display (hidden by default) -->
<ScrollViewer x:Name="ErrorPanel" Grid.Row="1" Visibility="Collapsed" Padding="20">
<StackPanel Spacing="12">
<TextBlock Text="WebView2 Failed to Initialize" FontSize="18" FontWeight="SemiBold"
<TextBlock Text="Web Chat Unavailable" FontSize="18" FontWeight="SemiBold"
Foreground="{ThemeResource SystemFillColorCriticalBrush}"/>
<TextBlock x:Name="ErrorText" TextWrapping="Wrap" IsTextSelectionEnabled="True"
FontFamily="Consolas" FontSize="12"/>

View File

@ -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 <mac>";
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
{

View File

@ -14,7 +14,7 @@
<AssemblyCompany>Scott Hanselman</AssemblyCompany>
<AssemblyProduct>OpenClaw Tray</AssemblyProduct>
<Copyright>Copyright © 2026 Scott Hanselman</Copyright>
<Version>0.4.1</Version>
<Version>0.4.2</Version>
<PublishSingleFile>true</PublishSingleFile>
<SelfContained>true</SelfContained>
<!-- RuntimeIdentifier set at publish time: win-x64 or win-arm64 -->