From 98eecfa757c2e3d27ce58994dcc2ff6547558cd1 Mon Sep 17 00:00:00 2001 From: Copilot Date: Tue, 7 Jul 2026 22:30:52 -0700 Subject: [PATCH] Add OpenClaw red-bot brand mark Introduce a shared BrandMark control for the tray UI and replace the old lobster/generic robot marks in title bars, dialogs, and the Agent Events empty state. Remove the dead FluentIconCatalog brand emoji and add source-text contracts for the red-bot mark. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Controls/BrandMark.xaml | 13 +++ .../Controls/BrandMark.xaml.cs | 47 ++++++++++ .../Dialogs/PairingApprovalDialog.cs | 3 +- .../Dialogs/RecordingConsentDialog.cs | 6 +- .../Dialogs/WelcomeDialog.cs | 11 ++- .../Helpers/BrandAssets.cs | 10 +++ .../Helpers/FluentIconCatalog.cs | 3 - .../Pages/AgentEventsPage.xaml | 3 +- .../Windows/HubWindow.xaml | 6 +- .../BrandMarkContractTests.cs | 85 +++++++++++++++++++ .../DiagnosticsPageContractTests.cs | 8 +- .../FluentIconCatalogTests.cs | 11 +-- 12 files changed, 180 insertions(+), 26 deletions(-) create mode 100644 src/OpenClaw.Tray.WinUI/Controls/BrandMark.xaml create mode 100644 src/OpenClaw.Tray.WinUI/Controls/BrandMark.xaml.cs create mode 100644 src/OpenClaw.Tray.WinUI/Helpers/BrandAssets.cs create mode 100644 tests/OpenClaw.Tray.Tests/BrandMarkContractTests.cs diff --git a/src/OpenClaw.Tray.WinUI/Controls/BrandMark.xaml b/src/OpenClaw.Tray.WinUI/Controls/BrandMark.xaml new file mode 100644 index 000000000..bb7b07b10 --- /dev/null +++ b/src/OpenClaw.Tray.WinUI/Controls/BrandMark.xaml @@ -0,0 +1,13 @@ + + + + + diff --git a/src/OpenClaw.Tray.WinUI/Controls/BrandMark.xaml.cs b/src/OpenClaw.Tray.WinUI/Controls/BrandMark.xaml.cs new file mode 100644 index 000000000..dc8d2b2a9 --- /dev/null +++ b/src/OpenClaw.Tray.WinUI/Controls/BrandMark.xaml.cs @@ -0,0 +1,47 @@ +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using OpenClawTray.Helpers; + +namespace OpenClawTray.Controls; + +public sealed partial class BrandMark : UserControl +{ + private const double DefaultMarkSize = 20d; + + public static readonly DependencyProperty MarkSizeProperty = + DependencyProperty.Register( + nameof(MarkSize), + typeof(double), + typeof(BrandMark), + new PropertyMetadata(DefaultMarkSize, OnMarkSizeChanged)); + + public BrandMark() + { + InitializeComponent(); + MarkImage.Source = BrandAssets.CreateRedBotMarkSource(); + ApplySize(MarkSize); + } + + public double MarkSize + { + get => (double)GetValue(MarkSizeProperty); + set => SetValue(MarkSizeProperty, value); + } + + private static void OnMarkSizeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is BrandMark mark && e.NewValue is double size) + mark.ApplySize(size); + } + + private void ApplySize(double size) + { + if (!double.IsFinite(size) || size < 0) + size = DefaultMarkSize; + + Width = size; + Height = size; + MarkImage.Width = size; + MarkImage.Height = size; + } +} diff --git a/src/OpenClaw.Tray.WinUI/Dialogs/PairingApprovalDialog.cs b/src/OpenClaw.Tray.WinUI/Dialogs/PairingApprovalDialog.cs index 9d4613276..a864a1d2b 100644 --- a/src/OpenClaw.Tray.WinUI/Dialogs/PairingApprovalDialog.cs +++ b/src/OpenClaw.Tray.WinUI/Dialogs/PairingApprovalDialog.cs @@ -3,6 +3,7 @@ using Microsoft.UI.Xaml.Media; using OpenClaw.Connection; using OpenClaw.Shared; +using OpenClawTray.Controls; using OpenClawTray.Helpers; using OpenClawTray.Services; using System; @@ -65,7 +66,7 @@ public PairingApprovalDialog(PairingApprovalCoordinator coordinator) var titleBar = new Grid { Height = 48, Padding = new Thickness(16, 0, 140, 0) }; titleBar.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); titleBar.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); - var titleIcon = new TextBlock { Text = "🦞", FontSize = 16, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 8, 0) }; + var titleIcon = new BrandMark { MarkSize = 16, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 8, 0) }; Grid.SetColumn(titleIcon, 0); titleBar.Children.Add(titleIcon); var titleText = new TextBlock diff --git a/src/OpenClaw.Tray.WinUI/Dialogs/RecordingConsentDialog.cs b/src/OpenClaw.Tray.WinUI/Dialogs/RecordingConsentDialog.cs index db2bdc7e2..28f80d9b5 100644 --- a/src/OpenClaw.Tray.WinUI/Dialogs/RecordingConsentDialog.cs +++ b/src/OpenClaw.Tray.WinUI/Dialogs/RecordingConsentDialog.cs @@ -2,6 +2,7 @@ using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Media; using OpenClaw.Shared.Capabilities; +using OpenClawTray.Controls; using OpenClawTray.Helpers; using OpenClawTray.Services; using System; @@ -55,10 +56,9 @@ public RecordingConsentDialog(RecordingType type) titleBar.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); titleBar.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); - var titleIcon = new TextBlock + var titleIcon = new BrandMark { - Text = "🦞", - FontSize = 16, + MarkSize = 16, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 8, 0) }; diff --git a/src/OpenClaw.Tray.WinUI/Dialogs/WelcomeDialog.cs b/src/OpenClaw.Tray.WinUI/Dialogs/WelcomeDialog.cs index 2971a7acf..0744e8321 100644 --- a/src/OpenClaw.Tray.WinUI/Dialogs/WelcomeDialog.cs +++ b/src/OpenClaw.Tray.WinUI/Dialogs/WelcomeDialog.cs @@ -1,6 +1,7 @@ using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Media; +using OpenClawTray.Controls; using OpenClawTray.Helpers; using OpenClawTray.Services; using System; @@ -45,10 +46,9 @@ public WelcomeDialog() Spacing = 12, HorizontalAlignment = HorizontalAlignment.Center }; - headerPanel.Children.Add(new TextBlock + headerPanel.Children.Add(new BrandMark { - Text = "🦞", - FontSize = 48 + MarkSize = 48 }); headerPanel.Children.Add(new TextBlock { @@ -130,10 +130,9 @@ public WelcomeDialog() outerGrid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) }); var titleBar = new Grid { Padding = new Thickness(16, 0, 140, 0) }; - var titleIcon = new TextBlock + var titleIcon = new BrandMark { - Text = "🦞", - FontSize = 20, + MarkSize = 20, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 10, 0) }; diff --git a/src/OpenClaw.Tray.WinUI/Helpers/BrandAssets.cs b/src/OpenClaw.Tray.WinUI/Helpers/BrandAssets.cs new file mode 100644 index 000000000..bb4e7b10c --- /dev/null +++ b/src/OpenClaw.Tray.WinUI/Helpers/BrandAssets.cs @@ -0,0 +1,10 @@ +using Microsoft.UI.Xaml.Media.Imaging; + +namespace OpenClawTray.Helpers; + +public static class BrandAssets +{ + public const string RedBotMarkUri = "ms-appx:///Assets/Square44x44Logo.targetsize-256_altform-unplated.png"; + + public static BitmapImage CreateRedBotMarkSource() => new(new Uri(RedBotMarkUri)); +} diff --git a/src/OpenClaw.Tray.WinUI/Helpers/FluentIconCatalog.cs b/src/OpenClaw.Tray.WinUI/Helpers/FluentIconCatalog.cs index 608c646ff..117a5ea05 100644 --- a/src/OpenClaw.Tray.WinUI/Helpers/FluentIconCatalog.cs +++ b/src/OpenClaw.Tray.WinUI/Helpers/FluentIconCatalog.cs @@ -73,9 +73,6 @@ public static class FluentIconCatalog public const string ChevronR = "\uE76C"; // ChevronRight public const string Check = "\uE73E"; // CheckMark - // ── Brand placeholder ────────────────────────────────────────── - public const string Brand = "🦞"; - // ── Diagnostics page glyphs ──────────────────────────────────── // Added 2026-05 for the Diagnostics surface // (src/OpenClaw.Tray.WinUI/Pages/DebugPage.xaml). Keep these here diff --git a/src/OpenClaw.Tray.WinUI/Pages/AgentEventsPage.xaml b/src/OpenClaw.Tray.WinUI/Pages/AgentEventsPage.xaml index badd718fa..594fb8211 100644 --- a/src/OpenClaw.Tray.WinUI/Pages/AgentEventsPage.xaml +++ b/src/OpenClaw.Tray.WinUI/Pages/AgentEventsPage.xaml @@ -3,6 +3,7 @@ x:Class="OpenClawTray.Pages.AgentEventsPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:controls="using:OpenClawTray.Controls" xmlns:helpers="using:OpenClawTray.Helpers"> @@ -140,7 +141,7 @@ - + diff --git a/src/OpenClaw.Tray.WinUI/Windows/HubWindow.xaml b/src/OpenClaw.Tray.WinUI/Windows/HubWindow.xaml index 7b67288ff..5250f8b5c 100644 --- a/src/OpenClaw.Tray.WinUI/Windows/HubWindow.xaml +++ b/src/OpenClaw.Tray.WinUI/Windows/HubWindow.xaml @@ -3,6 +3,7 @@ x:Class="OpenClawTray.Windows.HubWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:controls="using:OpenClawTray.Controls" xmlns:winex="using:WinUIEx" Title="OpenClaw" MinWidth="1000" MinHeight="400"> @@ -60,7 +61,10 @@ - + diff --git a/tests/OpenClaw.Tray.Tests/BrandMarkContractTests.cs b/tests/OpenClaw.Tray.Tests/BrandMarkContractTests.cs new file mode 100644 index 000000000..973a629d8 --- /dev/null +++ b/tests/OpenClaw.Tray.Tests/BrandMarkContractTests.cs @@ -0,0 +1,85 @@ +namespace OpenClaw.Tray.Tests; + +public sealed class BrandMarkContractTests +{ + private const string LobsterEmoji = "\ud83e\udd9e"; + private const string RobotEmoji = "\ud83e\udd16"; + private const string RedBotAssetName = "Square44x44Logo.targetsize-256_altform-unplated.png"; + private static string RepoRoot => TestRepositoryPaths.GetRepositoryRoot(); + + private static string ReadTrayFile(params string[] pathParts) + => File.ReadAllText(Path.Combine(new[] { RepoRoot, "src", "OpenClaw.Tray.WinUI" }.Concat(pathParts).ToArray())); + + [Fact] + public void BrandMark_UsesSharedRedBotImageAsset() + { + var xaml = ReadTrayFile("Controls", "BrandMark.xaml"); + var code = ReadTrayFile("Controls", "BrandMark.xaml.cs"); + var assets = ReadTrayFile("Helpers", "BrandAssets.cs"); + var redBotAssetPath = Path.Combine( + RepoRoot, + "src", + "OpenClaw.Tray.WinUI", + "Assets", + RedBotAssetName); + + Assert.Contains(" path.EndsWith(".cs", StringComparison.OrdinalIgnoreCase) + || path.EndsWith(".xaml", StringComparison.OrdinalIgnoreCase)) + .Where(path => !IsBuildArtifact(sourceRoot, path)) + .SelectMany(path => File.ReadLines(path) + .Select((line, index) => (path, line, lineNumber: index + 1))) + .Where(item => item.line.Contains(LobsterEmoji, StringComparison.Ordinal) + || item.line.Contains(RobotEmoji, StringComparison.Ordinal)) + .Select(item => $"{Path.GetRelativePath(RepoRoot, item.path)}:{item.lineNumber}") + .ToArray(); + + Assert.True( + offenders.Length == 0, + "Use the shared BrandMark control instead of emoji brand marks:" + + Environment.NewLine + + string.Join(Environment.NewLine, offenders)); + + static bool IsBuildArtifact(string sourceRoot, string path) + { + var relative = Path.GetRelativePath(sourceRoot, path); + return relative.StartsWith($"bin{Path.DirectorySeparatorChar}", StringComparison.OrdinalIgnoreCase) + || relative.StartsWith($"obj{Path.DirectorySeparatorChar}", StringComparison.OrdinalIgnoreCase); + } + } +} diff --git a/tests/OpenClaw.Tray.Tests/DiagnosticsPageContractTests.cs b/tests/OpenClaw.Tray.Tests/DiagnosticsPageContractTests.cs index 1b09e19cf..a41eec75a 100644 --- a/tests/OpenClaw.Tray.Tests/DiagnosticsPageContractTests.cs +++ b/tests/OpenClaw.Tray.Tests/DiagnosticsPageContractTests.cs @@ -439,8 +439,10 @@ public void HubWindow_NavPaneToggle_LivesInTitleBarAndHidesBuiltInToggle() Assert.Contains("BorderBrush=\"Transparent\"", xaml); Assert.Contains("BorderThickness=\"0\"", xaml); Assert.Contains("FontSize=\"16\"", xaml); - Assert.Contains("Text=\"\ud83e\udd9e\" FontSize=\"18\"", xaml); - Assert.Contains("Translation=\"0,-1,0\"", xaml); + Assert.Contains("xmlns:controls=\"using:OpenClawTray.Controls\"", xaml); + Assert.Contains("= 0, "The hub title bar must exist."); Assert.True(toggleIndex > titleBarIndex, "The nav pane toggle must live inside the title bar block."); diff --git a/tests/OpenClaw.Tray.Tests/FluentIconCatalogTests.cs b/tests/OpenClaw.Tray.Tests/FluentIconCatalogTests.cs index 96e4c36cb..b6e796fb2 100644 --- a/tests/OpenClaw.Tray.Tests/FluentIconCatalogTests.cs +++ b/tests/OpenClaw.Tray.Tests/FluentIconCatalogTests.cs @@ -5,8 +5,8 @@ namespace OpenClaw.Tray.Tests; /// /// Pins the FluentIconCatalog contract: every advertised glyph is a -/// single Unicode Private Use Area character (or the documented Brand -/// emoji), and the helper builder uses the SymbolThemeFontFamily resource. +/// single Unicode Private Use Area character, and the helper builder uses +/// the SymbolThemeFontFamily resource. /// /// We parse the source rather than reflect on the assembly because /// OpenClaw.Tray.Tests is a pure net10.0 project that doesn't @@ -24,7 +24,6 @@ public sealed class FluentIconCatalogTests "Add", "Back", "Sync", "Lock", "Plug", "MoreOverflow", "People", "Money", "ServerEnvironment", "CapabilityOff", "Channels", "ChevronR", "Check", - "Brand", // Diagnostics surface (see src/OpenClaw.Tray.WinUI/Pages/DebugPage.xaml). "Bug", "Briefcase", "Folder", "Copy", "Document", "Refresh", "Reset", "Clear", "Develop", // Workspace surface (see src/OpenClaw.Tray.WinUI/Pages/WorkspacePage.xaml). @@ -41,8 +40,7 @@ private static string ReadCatalogSource() private static IDictionary ParseConstants(string source) { - // Matches: public const string Name = "\uXXXX"; or - // public const string Name = "🦞"; + // Matches: public const string Name = "\uXXXX"; var rx = new Regex( @"public\s+const\s+string\s+(?\w+)\s*=\s*""(?(?:\\u[0-9A-Fa-f]{4}|[^""\\]|\\.)*)"";", RegexOptions.Compiled); @@ -74,9 +72,6 @@ public void PuaConstants_AreSingleCharacterInPrivateUseArea() var map = ParseConstants(src); foreach (var name in ExpectedConstants) { - if (name == "Brand") - continue; // Brand is an emoji surrogate pair by design. - Assert.True(map.TryGetValue(name, out var value), $"FluentIconCatalog.{name} not found in source"); Assert.True(value!.Length == 1,