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,