chore: remove WinForms tray app (OpenClaw.Tray) (#50)

WinUI tray has full feature parity plus Node Mode, Activity Stream,
session management, cost tracking, localization, and modern XAML UI.

- Remove src/OpenClaw.Tray/ (28 files, ~4900 lines)
- Remove from solution file
- Remove WinForms build step from CI

Installer already only references WinUI. Settings.json format is
unchanged — no migration needed.

Closes #44
This commit is contained in:
Scott Hanselman 2026-03-16 21:58:19 -07:00 committed by GitHub
parent 313e69e483
commit 8e4c7bd587
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 0 additions and 4929 deletions

View File

@ -35,9 +35,6 @@ jobs:
- name: Build Shared Library
run: dotnet build src/OpenClaw.Shared -c Debug
- name: Build Tray App (WinForms)
run: dotnet build src/OpenClaw.Tray -c Debug
- name: Build Tray App (WinUI)
run: dotnet build src/OpenClaw.Tray.WinUI -c Debug -r win-x64

View File

@ -4,7 +4,6 @@
<Platform Project="x64" />
</Project>
<Project Path="src/OpenClaw.Shared/OpenClaw.Shared.csproj" />
<Project Path="src/OpenClaw.Tray/OpenClaw.Tray.csproj" />
<Project Path="src/OpenClaw.Tray.WinUI/OpenClaw.Tray.WinUI.csproj" />
</Folder>
<Folder Name="/tests/">

View File

@ -1,345 +0,0 @@
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Oo]ut/
[Ll]og/
[Ll]ogs/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Coverlet is a free, cross platform Code Coverage Tool
coverage*.json
coverage*.xml
coverage*.info
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these files may be extracted
*.azurePubxml
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment the next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
CrystalDecisions.ReportingServices.ViewerObjectModel.dll
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd

View File

@ -1,59 +0,0 @@
using Microsoft.Win32;
using System;
using System.IO;
using System.Reflection;
using System.Windows.Forms;
namespace OpenClawTray;
public static class AutoStartManager
{
private const string RegistryKeyPath = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Run";
private const string ApplicationName = "OpenClawTray";
public static void SetAutoStart(bool enabled)
{
try
{
using var key = Registry.CurrentUser.OpenSubKey(RegistryKeyPath, true);
if (key != null)
{
if (enabled)
{
var exePath = GetExecutablePath();
key.SetValue(ApplicationName, $"\"{exePath}\"");
}
else
{
key.DeleteValue(ApplicationName, false);
}
}
}
catch (Exception ex)
{
MessageBox.Show($"Failed to update auto-start setting: {ex.Message}",
"Auto-start Error", MessageBoxButtons.OK, MessageBoxIcon.Warning);
}
}
public static bool IsAutoStartEnabled()
{
try
{
using var key = Registry.CurrentUser.OpenSubKey(RegistryKeyPath);
return key?.GetValue(ApplicationName) != null;
}
catch
{
return false;
}
}
private static string GetExecutablePath()
{
// Use ProcessPath for single-file deployments (Assembly.Location is empty)
var location = Environment.ProcessPath ?? Application.ExecutablePath;
return location;
}
}

View File

@ -1,169 +0,0 @@
# Development Notes
## Architecture Overview
This Windows system tray application is built with .NET 10 and Windows Forms, designed to be lightweight and efficient while providing seamless integration with the OpenClaw gateway. It mirrors the macOS menu bar app's functionality for Windows users.
### Component Architecture
```
┌──────────────┐ ┌─────────────────────────┐
│ Program │────▶│ TrayApplication │
│ (entry) │ │ - System tray icon │
│ - Mutex │ │ - Context menu │
│ - URI reg │ │ - Event dispatch (UI) │
└──────────────┘ │ - Session awareness │
└────────┬────────────────┘
│ events
┌────────▼────────────────┐
│ OpenClawGatewayClient │
│ - WebSocket connection │
│ - Protocol v3 handshake │
│ - Event parsing │
│ - Session/usage tracking│
│ - Auto-reconnect │
└─────────────────────────┘
```
### Key Components
| Component | File | Purpose |
|-----------|------|---------|
| **Program** | `Program.cs` | Entry point, single-instance mutex, URI scheme registration |
| **TrayApplication** | `TrayApplication.cs` | Main `ApplicationContext` managing the tray icon, context menu, and UI event dispatch |
| **OpenClawGatewayClient** | `OpenClawGatewayClient.cs` | WebSocket client implementing gateway protocol v3 with event parsing, session tracking, and usage monitoring |
| **SettingsManager** | `SettingsManager.cs` | JSON-based settings persistence in `%APPDATA%\OpenClawTray\` |
| **SettingsDialog** | `SettingsDialog.cs` | Settings UI with URL/token config, test connection (with timeout), and notification preferences |
| **Logger** | `Logger.cs` | Thread-safe file + debug logger with automatic rotation (1MB), writes to `%LOCALAPPDATA%\OpenClawTray\openclaw-tray.log` |
| **DeepLinkHandler** | `DeepLinkHandler.cs` | `openclaw://` URI scheme registration and processing for cross-app integration |
| **WebChatForm** | `WebChatForm.cs` | WebView2-based chat panel (singleton) with toolbar, fallback to browser |
| **QuickSendDialog** | `QuickSendDialog.cs` | Lightweight dialog for sending messages (supports Ctrl+Enter) |
| **StatusDetailForm** | `StatusDetailForm.cs` | Rich status view showing gateway connection, sessions, channels, usage, and app info |
| **NotificationHistoryForm** | `NotificationHistoryForm.cs` | Scrollable history of received notifications |
| **AutoStartManager** | `AutoStartManager.cs` | Windows startup integration via `HKCU\...\Run` registry key |
| **GlobalHotkey** | `GlobalHotkey.cs` | System-wide hotkey registration for quick access |
### Data Flow
1. **Gateway → Client**: WebSocket messages parsed into typed events (`agent`, `chat`, `health`, `session`, `usage`)
2. **Client → TrayApp**: C# events marshaled to UI thread via `SynchronizationContext.Post`
3. **TrayApp → UI**: Context menu items, tray icon, and toast notifications updated
### Event Types from Gateway
| Event | Handler | UI Result |
|-------|---------|-----------|
| `agent` (stream=job) | `HandleJobEvent` | Activity row update, icon badge |
| `agent` (stream=tool) | `HandleToolEvent` | Activity row with tool name + args detail |
| `chat` | `HandleChatEvent` | Toast notification for short assistant messages |
| `health` | `ParseChannelHealth` | Channel health rows in context menu |
| `session` | `HandleSessionEvent` | Session list refresh |
| `usage` | `ParseUsage` | Usage row (tokens, model, cost) |
### Notification Classification
Notifications are classified two ways:
1. **Structured** (preferred): Events with explicit `type`, `category`, or `notificationType` fields
2. **Text-based** (fallback): Keyword matching on notification content (glucose, reminder, stock, email, calendar, etc.)
### Session Awareness
The activity display uses a stable session selection algorithm:
1. Active main session always takes priority
2. Currently displayed session is kept if still active (prevents flip-flop)
3. Falls back to most recently active sub-session
4. 3-second debounce window prevents jitter during rapid activity changes
### Tray Icon
The tray icon is a programmatically drawn 16×16 circle:
- **Teal**: Connected
- **Amber**: Connecting
- **Red**: Error
- **Gray**: Disconnected
An activity badge (small corner dot) appears during tool execution:
- **Orange**: exec (running commands)
- **Green**: write/edit (file changes)
- **Blue**: read (file access)
- **Purple**: search/browser (web activity)
### Settings Storage
Settings are stored as JSON in `%APPDATA%\OpenClawTray\settings.json`:
```json
{
"GatewayUrl": "ws://localhost:18789",
"Token": "...",
"AutoStart": false,
"ShowNotifications": true,
"NotificationSound": "Default"
}
```
### Deep Links
The app registers `openclaw://` URI scheme for cross-app integration:
```
openclaw://agent?message=Hello&key=optional-auth-key
```
Without a key, the user is prompted before sending. With a key, the message is sent directly.
## Build & Development
### Prerequisites
- .NET 10 SDK
- Windows 10 SDK (19041+) — cross-compilation from Linux supported via `EnableWindowsTargeting`
- WebView2 Runtime (for chat panel, optional at runtime)
### Build
```bash
dotnet build
```
### Publish (single-file, self-contained)
```bash
dotnet publish -c Release -r win-x64 --self-contained -p:PublishSingleFile=true
```
### CI
GitHub Actions builds on every push. Check status:
```bash
gh run list --repo shanselman/moltbot-windows-hub --limit 1
```
## Dependencies
| Package | Purpose |
|---------|---------|
| `Microsoft.Toolkit.Uwp.Notifications` (7.1.3) | Toast notifications with rich content |
| `Microsoft.Web.WebView2` (1.0.3124.44) | Embedded browser for chat panel |
| `System.Text.Json` (9.0.0) | JSON serialization for settings and gateway protocol |
## Security Considerations
- **Token storage**: Plaintext in user AppData (future: Windows Credential Manager)
- **Deep links**: Untrusted deep links prompt user confirmation
- **WebSocket**: Supports both `ws://` (local) and `wss://` (remote)
- **Auto-start**: Registry-based, current user only (no elevation needed)
- **Logging**: Sensitive data (tokens) not logged
## Known Limitations
- Toast notifications require Windows 10 1903+
- WebView2 Runtime must be installed separately for chat panel
- Single-instance enforced via Mutex (deep link forwarding to running instance TODO)
- Tray icon tooltip limited to 63 characters (full detail shown in context menu activity row)

View File

@ -1,174 +0,0 @@
using Microsoft.Win32;
using OpenClaw.Shared;
using System;
using System.Collections.Specialized;
using System.Threading.Tasks;
using System.Web;
using System.Windows.Forms;
namespace OpenClawTray;
/// <summary>
/// Handles openclaw:// URI scheme registration and processing.
/// Matches macOS deep link support (openclaw://agent?message=...)
/// </summary>
public static class DeepLinkHandler
{
private const string UriScheme = "OpenClaw";
private const string FriendlyName = "OpenClaw Agent Command";
/// <summary>
/// Registers the openclaw:// URI scheme in the Windows registry.
/// Requires elevation for HKCR, falls back to HKCU.
/// </summary>
public static void RegisterUriScheme()
{
try
{
var exePath = Environment.ProcessPath ?? Application.ExecutablePath;
// Try HKCU\Software\Classes (no elevation needed)
using var key = Registry.CurrentUser.CreateSubKey($@"Software\Classes\{UriScheme}");
if (key == null) return;
key.SetValue("", $"URL:{FriendlyName}");
key.SetValue("URL Protocol", "");
using var iconKey = key.CreateSubKey("DefaultIcon");
iconKey?.SetValue("", $"\"{exePath}\",1");
using var commandKey = key.CreateSubKey(@"shell\open\command");
commandKey?.SetValue("", $"\"{exePath}\" \"%1\"");
Logger.Info($"Registered URI scheme: {UriScheme}://");
}
catch (Exception ex)
{
Logger.Error("Failed to register URI scheme", ex);
}
}
/// <summary>
/// Checks if the app was launched with a deep link argument.
/// </summary>
public static bool TryGetDeepLink(string[] args, out Uri? uri)
{
uri = null;
if (args.Length == 0) return false;
foreach (var arg in args)
{
if (arg.StartsWith($"{UriScheme}://", StringComparison.OrdinalIgnoreCase))
{
try
{
uri = new Uri(arg);
return true;
}
catch { }
}
}
return false;
}
/// <summary>
/// Processes a openclaw:// deep link.
/// Supports:
/// openclaw://agent?message=...
/// openclaw://send?message=... (opens Quick Send with pre-filled text)
/// openclaw://dashboard
/// openclaw://chat
/// openclaw://settings
/// </summary>
public static async Task ProcessDeepLinkAsync(Uri uri, OpenClawGatewayClient client, Action<string>? openDashboard = null, Action? openChat = null, Action? openSettings = null, Action<string>? openQuickSend = null)
{
Logger.Info($"Processing deep link: {uri}");
var host = uri.Host.ToLowerInvariant();
var query = HttpUtility.ParseQueryString(uri.Query);
switch (host)
{
case "agent":
await HandleAgentDeepLinkAsync(query, client);
break;
case "send":
var msg = query["message"] ?? "";
openQuickSend?.Invoke(msg);
break;
case "dashboard":
openDashboard?.Invoke(uri.AbsolutePath.TrimStart('/'));
break;
case "chat":
openChat?.Invoke();
break;
case "settings":
openSettings?.Invoke();
break;
default:
Logger.Warn($"Unknown deep link host: {host}");
break;
}
}
private static async Task HandleAgentDeepLinkAsync(NameValueCollection query, OpenClawGatewayClient client)
{
var message = query["message"];
if (string.IsNullOrWhiteSpace(message))
{
Logger.Warn("Deep link: missing message parameter");
return;
}
var key = query["key"];
var hasKey = !string.IsNullOrEmpty(key);
// Without a key, prompt for confirmation (safety)
if (!hasKey)
{
var preview = message.Length > 100 ? message[..100] + "…" : message;
var result = MessageBox.Show(
$"A deep link wants to send this message to OpenClaw:\n\n\"{preview}\"\n\nAllow?",
"OpenClaw Deep Link",
MessageBoxButtons.YesNo,
MessageBoxIcon.Question);
if (result != DialogResult.Yes)
{
Logger.Info("Deep link: user declined");
return;
}
}
try
{
await client.SendChatMessageAsync(message);
Logger.Info($"Deep link: sent message ({message.Length} chars)");
// Show confirmation toast
try
{
new Microsoft.Toolkit.Uwp.Notifications.ToastContentBuilder()
.AddText("🦞 Message Sent")
.AddText(message.Length > 50 ? message[..50] + "…" : message)
.Show();
}
catch { }
}
catch (Exception ex)
{
Logger.Error("Deep link: failed to send", ex);
// Show error toast
try
{
new Microsoft.Toolkit.Uwp.Notifications.ToastContentBuilder()
.AddText("❌ Failed to Send")
.AddText(ex.Message)
.Show();
}
catch { }
}
}
}

View File

@ -1,66 +0,0 @@
using System;
using System.ComponentModel;
using System.Drawing;
using System.Windows.Forms;
using Updatum;
namespace OpenClawTray;
public class DownloadProgressDialog : ModernForm
{
private readonly UpdatumManager _updater;
private readonly ProgressBar _progressBar;
private readonly Label _progressLabel;
public DownloadProgressDialog(UpdatumManager updater)
{
_updater = updater;
_updater.PropertyChanged += UpdaterOnPropertyChanged;
Text = "Downloading Update — OpenClaw Tray";
Size = new Size(420, 160);
ControlBox = false;
Icon = IconHelper.GetLobsterIcon();
var titleLabel = CreateModernLabel("🦞 Downloading update...");
titleLabel.Font = new Font("Segoe UI", 11, FontStyle.Bold);
titleLabel.ForeColor = AccentColor;
titleLabel.Location = new Point(20, 20);
Controls.Add(titleLabel);
_progressBar = CreateModernProgressBar();
_progressBar.Location = new Point(20, 60);
_progressBar.Size = new Size(364, 8);
Controls.Add(_progressBar);
_progressLabel = CreateModernLabel("Starting download...", isSubtle: true);
_progressLabel.Location = new Point(20, 78);
_progressLabel.Size = new Size(364, 24);
_progressLabel.TextAlign = ContentAlignment.MiddleCenter;
Controls.Add(_progressLabel);
}
private void UpdaterOnPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(UpdatumManager.DownloadedPercentage))
{
if (InvokeRequired)
Invoke(() => UpdateProgress());
else
UpdateProgress();
}
}
private void UpdateProgress()
{
_progressBar.Value = (int)Math.Min(_updater.DownloadedPercentage, 100);
_progressLabel.Text = $"{_updater.DownloadedMegabytes:F2} MB / {_updater.DownloadSizeMegabytes:F2} MB ({_updater.DownloadedPercentage:F1}%)";
}
protected override void OnFormClosing(FormClosingEventArgs e)
{
_updater.PropertyChanged -= UpdaterOnPropertyChanged;
base.OnFormClosing(e);
}
}

View File

@ -1,94 +0,0 @@
using System;
using System.Runtime.InteropServices;
using System.Windows.Forms;
namespace OpenClawTray;
/// <summary>
/// Registers a system-wide hotkey that works even when the app is not focused.
/// Default: Ctrl+Alt+Shift+C to open Quick Send.
/// </summary>
public class GlobalHotkey : IDisposable
{
[DllImport("user32.dll")]
private static extern bool RegisterHotKey(IntPtr hWnd, int id, uint fsModifiers, uint vk);
[DllImport("user32.dll")]
private static extern bool UnregisterHotKey(IntPtr hWnd, int id);
private const int HotkeyId = 9001;
private const uint MOD_ALT = 0x0001;
private const uint MOD_CONTROL = 0x0002;
private const uint MOD_SHIFT = 0x0004;
private const uint VK_C = 0x43;
private readonly HotkeyWindow _window;
private bool _registered;
public event EventHandler? HotkeyPressed;
public GlobalHotkey()
{
_window = new HotkeyWindow(this);
}
public bool Register()
{
try
{
_registered = RegisterHotKey(_window.Handle, HotkeyId, MOD_CONTROL | MOD_ALT | MOD_SHIFT, VK_C);
if (_registered)
Logger.Info("Global hotkey registered: Ctrl+Alt+Shift+C");
else
Logger.Warn("Failed to register global hotkey (may be in use by another app)");
return _registered;
}
catch (Exception ex)
{
Logger.Error("Hotkey registration error", ex);
return false;
}
}
public void Dispose()
{
if (_registered)
{
UnregisterHotKey(_window.Handle, HotkeyId);
_registered = false;
}
_window.Dispose();
}
internal void OnHotkeyPressed()
{
HotkeyPressed?.Invoke(this, EventArgs.Empty);
}
private class HotkeyWindow : NativeWindow, IDisposable
{
private const int WM_HOTKEY = 0x0312;
private readonly GlobalHotkey _owner;
public HotkeyWindow(GlobalHotkey owner)
{
_owner = owner;
CreateHandle(new CreateParams());
}
protected override void WndProc(ref Message m)
{
if (m.Msg == WM_HOTKEY && m.WParam.ToInt32() == HotkeyId)
{
_owner.OnHotkeyPressed();
}
base.WndProc(ref m);
}
public void Dispose()
{
DestroyHandle();
}
}
}

View File

@ -1,95 +0,0 @@
using System.Drawing;
namespace OpenClawTray;
/// <summary>
/// Shared icon helper for creating the lobster icon used throughout the app.
/// </summary>
public static class IconHelper
{
private static Icon? _cachedLobsterIcon;
/// <summary>
/// Gets the lobster icon for use in forms and windows.
/// </summary>
public static Icon GetLobsterIcon()
{
if (_cachedLobsterIcon != null)
return _cachedLobsterIcon;
var bitmap = new Bitmap(16, 16);
using (var g = Graphics.FromImage(bitmap))
{
g.Clear(Color.Transparent);
DrawPixelLobster(g);
}
var hIcon = bitmap.GetHicon();
_cachedLobsterIcon = Icon.FromHandle(hIcon);
return _cachedLobsterIcon;
}
private static void DrawPixelLobster(Graphics g)
{
// Pixel lobster from SVG - 16x16 pixel art
var outline = Color.FromArgb(58, 10, 13); // #3a0a0d - dark outline
var body = Color.FromArgb(255, 79, 64); // #ff4f40 - red body
var claw = Color.FromArgb(255, 119, 95); // #ff775f - lighter claws
var eyeDark = Color.FromArgb(8, 16, 22); // #081016 - pupils
var eyeLight = Color.FromArgb(245, 251, 255); // #f5fbff - eye whites
// Outline (dark border)
var outlinePixels = new[] {
(1,5), (1,6), (1,7),
(2,4), (2,8),
(3,3), (3,9),
(4,2), (4,10),
(5,2), (6,2), (7,2), (8,2), (9,2), (10,2),
(11,2), (12,3), (12,9),
(13,4), (13,8),
(14,5), (14,6), (14,7),
(5,11), (6,11), (7,11), (8,11), (9,11), (10,11),
(4,12), (11,12),
(3,13), (12,13),
(5,14), (6,14), (7,14), (8,14), (9,14), (10,14)
};
foreach (var (x, y) in outlinePixels)
SetPixel(g, x, y, outline);
// Body (red)
var bodyPixels = new[] {
(5,3), (6,3), (7,3), (8,3), (9,3), (10,3),
(4,4), (5,4), (7,4), (8,4), (10,4), (11,4),
(3,5), (4,5), (5,5), (7,5), (8,5), (10,5), (11,5), (12,5),
(3,6), (4,6), (5,6), (6,6), (7,6), (8,6), (9,6), (10,6), (11,6), (12,6),
(3,7), (4,7), (5,7), (6,7), (7,7), (8,7), (9,7), (10,7), (11,7), (12,7),
(4,8), (5,8), (6,8), (7,8), (8,8), (9,8), (10,8), (11,8),
(5,9), (6,9), (7,9), (8,9), (9,9), (10,9),
(5,12), (6,12), (7,12), (8,12), (9,12), (10,12),
(6,13), (7,13), (8,13), (9,13)
};
foreach (var (x, y) in bodyPixels)
SetPixel(g, x, y, body);
// Claws (lighter red)
var clawPixels = new[] {
(1,6), (2,5), (2,6), (2,7),
(13,5), (13,6), (13,7), (14,6)
};
foreach (var (x, y) in clawPixels)
SetPixel(g, x, y, claw);
// Eyes
SetPixel(g, 6, 4, eyeLight);
SetPixel(g, 9, 4, eyeLight);
SetPixel(g, 6, 5, eyeDark);
SetPixel(g, 9, 5, eyeDark);
}
private static void SetPixel(Graphics g, int x, int y, Color c)
{
using var brush = new SolidBrush(c);
g.FillRectangle(brush, x, y, 1, 1);
}
}

View File

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2025 Scott Hanselman
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1,109 +0,0 @@
using System;
using System.Diagnostics;
using System.IO;
using OpenClaw.Shared;
namespace OpenClawTray;
/// <summary>
/// Simple file + debug logger for troubleshooting.
/// Writes to %LOCALAPPDATA%\OpenClawTray\openclaw-tray.log
/// </summary>
public static class Logger
{
private static readonly string LogDir = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"OpenClawTray");
private static readonly string LogPath = Path.Combine(LogDir, "openclaw-tray.log");
private static readonly object Lock = new();
private static bool _initialized;
private static StreamWriter? _writer;
/// <summary>Get a logger instance that implements IOpenClawLogger for the shared library.</summary>
public static IOpenClawLogger Instance { get; } = new LoggerAdapter();
public static void Info(string message) => Write("INFO", message);
public static void Debug(string message) => Write("DEBUG", message);
public static void Warn(string message) => Write("WARN", message);
public static void Error(string message) => Write("ERROR", message);
public static void Error(string message, Exception ex) => Write("ERROR", $"{message}: {ex.Message}\n Stack: {ex.StackTrace}");
/// <summary>Flush and close the log file (call on app exit).</summary>
public static void Shutdown()
{
lock (Lock)
{
_writer?.Flush();
_writer?.Dispose();
_writer = null;
_initialized = false;
}
}
private static void EnsureInitialized()
{
if (_initialized) return;
try
{
Directory.CreateDirectory(LogDir);
RotateIfNeeded();
_writer = new StreamWriter(LogPath, append: true) { AutoFlush = true };
_initialized = true;
}
catch
{
// Can't init — fall back to Debug.WriteLine only
}
}
private static void RotateIfNeeded()
{
try
{
var info = new FileInfo(LogPath);
if (info.Exists && info.Length > 1_048_576)
{
var backup = LogPath + ".1";
if (File.Exists(backup)) File.Delete(backup);
File.Move(LogPath, backup);
}
}
catch { }
}
private static void Write(string level, string message)
{
var line = $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] [{level}] {message}";
System.Diagnostics.Debug.WriteLine(line);
try
{
lock (Lock)
{
EnsureInitialized();
_writer?.WriteLine(line);
}
}
catch
{
// Don't crash if we can't write logs
}
}
/// <summary>Adapter to make the static Logger work with IOpenClawLogger interface.</summary>
private class LoggerAdapter : IOpenClawLogger
{
public void Info(string message) => Logger.Info(message);
public void Debug(string message) => Logger.Debug(message);
public void Warn(string message) => Logger.Warn(message);
public void Error(string message, Exception? ex = null)
{
if (ex != null)
Logger.Error(message, ex);
else
Logger.Error(message);
}
}
}

View File

@ -1,257 +0,0 @@
using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Runtime.InteropServices;
using System.Windows.Forms;
using Microsoft.Win32;
namespace OpenClawTray;
/// <summary>
/// Base form with Windows 11 modern styling - dark/light mode, rounded corners, OpenClaw branding.
/// Inherit from this for consistent look across all dialogs.
/// </summary>
public class ModernForm : Form
{
[DllImport("dwmapi.dll")]
private static extern int DwmSetWindowAttribute(IntPtr hwnd, int attr, ref int attrValue, int attrSize);
private const int DWMWA_USE_IMMERSIVE_DARK_MODE = 20;
private const int DWMWA_WINDOW_CORNER_PREFERENCE = 33;
private const int DWMWCP_ROUND = 2;
// Theme colors - exposed for child controls
protected bool IsDarkMode { get; private set; }
protected Color AccentColor => Color.FromArgb(220, 53, 53); // Lobster red
protected Color BackgroundColor { get; private set; }
protected Color ForegroundColor { get; private set; }
protected Color SurfaceColor { get; private set; }
protected Color BorderColor { get; private set; }
protected Color HoverColor { get; private set; }
protected Color SubtleTextColor { get; private set; }
public ModernForm()
{
DetectTheme();
// Base form styling
Font = new Font("Segoe UI", 9.5f);
StartPosition = FormStartPosition.CenterScreen;
FormBorderStyle = FormBorderStyle.FixedDialog;
MaximizeBox = false;
MinimizeBox = false;
}
private void DetectTheme()
{
try
{
using var key = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize");
var value = key?.GetValue("AppsUseLightTheme");
IsDarkMode = value is int i && i == 0;
}
catch
{
IsDarkMode = false;
}
if (IsDarkMode)
{
BackgroundColor = Color.FromArgb(32, 32, 32);
ForegroundColor = Color.FromArgb(255, 255, 255);
SurfaceColor = Color.FromArgb(45, 45, 48);
BorderColor = Color.FromArgb(60, 60, 60);
HoverColor = Color.FromArgb(55, 55, 58);
SubtleTextColor = Color.FromArgb(180, 180, 180);
}
else
{
BackgroundColor = Color.FromArgb(249, 249, 249);
ForegroundColor = Color.FromArgb(28, 28, 28);
SurfaceColor = Color.FromArgb(255, 255, 255);
BorderColor = Color.FromArgb(200, 200, 200);
HoverColor = Color.FromArgb(229, 229, 229);
SubtleTextColor = Color.FromArgb(100, 100, 100);
}
BackColor = BackgroundColor;
ForeColor = ForegroundColor;
}
protected override void OnHandleCreated(EventArgs e)
{
base.OnHandleCreated(e);
ApplyModernStyling();
}
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
// Apply theme colors to all child controls
ApplyThemeToControls(Controls);
}
private void ApplyThemeToControls(Control.ControlCollection controls)
{
foreach (Control ctrl in controls)
{
// Skip controls that have explicit colors set (like accent-colored labels)
if (ctrl.ForeColor == AccentColor) continue;
// Apply foreground color to labels and checkboxes
if (ctrl is Label || ctrl is CheckBox || ctrl is RadioButton)
{
if (ctrl.ForeColor == Color.Black || ctrl.ForeColor == SystemColors.ControlText)
ctrl.ForeColor = ForegroundColor;
}
// Recurse into containers
if (ctrl.HasChildren)
ApplyThemeToControls(ctrl.Controls);
}
}
private void ApplyModernStyling()
{
// Enable Windows 11 rounded corners
int preference = DWMWCP_ROUND;
DwmSetWindowAttribute(Handle, DWMWA_WINDOW_CORNER_PREFERENCE, ref preference, sizeof(int));
// Enable dark mode title bar
int darkMode = IsDarkMode ? 1 : 0;
DwmSetWindowAttribute(Handle, DWMWA_USE_IMMERSIVE_DARK_MODE, ref darkMode, sizeof(int));
}
/// <summary>
/// Creates a styled button with OpenClaw branding.
/// </summary>
protected Button CreateModernButton(string text, bool isPrimary = false)
{
var btn = new Button
{
Text = text,
FlatStyle = FlatStyle.Flat,
Font = new Font("Segoe UI", 9.5f, isPrimary ? FontStyle.Bold : FontStyle.Regular),
Cursor = Cursors.Hand,
Height = 32,
Padding = new Padding(12, 0, 12, 0)
};
if (isPrimary)
{
btn.BackColor = AccentColor;
btn.ForeColor = Color.White;
btn.FlatAppearance.BorderSize = 0;
btn.FlatAppearance.MouseOverBackColor = Color.FromArgb(200, 43, 43);
}
else
{
btn.BackColor = SurfaceColor;
btn.ForeColor = ForegroundColor;
btn.FlatAppearance.BorderColor = BorderColor;
btn.FlatAppearance.BorderSize = 1;
btn.FlatAppearance.MouseOverBackColor = HoverColor;
}
return btn;
}
/// <summary>
/// Creates a styled text box.
/// </summary>
protected TextBox CreateModernTextBox()
{
return new TextBox
{
Font = new Font("Segoe UI", 10f),
BackColor = SurfaceColor,
ForeColor = ForegroundColor,
BorderStyle = BorderStyle.FixedSingle
};
}
/// <summary>
/// Creates a styled label.
/// </summary>
protected Label CreateModernLabel(string text, bool isSubtle = false)
{
return new Label
{
Text = text,
Font = new Font("Segoe UI", 9.5f),
ForeColor = isSubtle ? SubtleTextColor : ForegroundColor,
AutoSize = true
};
}
/// <summary>
/// Creates a styled checkbox.
/// </summary>
protected CheckBox CreateModernCheckBox(string text)
{
var cb = new CheckBox
{
Text = text,
Font = new Font("Segoe UI", 9.5f),
ForeColor = ForegroundColor,
BackColor = Color.Transparent,
AutoSize = true,
FlatStyle = FlatStyle.Standard
};
return cb;
}
/// <summary>
/// Creates a styled group box.
/// </summary>
protected GroupBox CreateModernGroupBox(string text)
{
return new GroupBox
{
Text = text,
Font = new Font("Segoe UI", 9.5f, FontStyle.Bold),
ForeColor = AccentColor,
BackColor = Color.Transparent
};
}
/// <summary>
/// Creates a styled panel with border.
/// </summary>
protected Panel CreateModernPanel()
{
return new Panel
{
BackColor = SurfaceColor,
BorderStyle = BorderStyle.None,
Padding = new Padding(12)
};
}
/// <summary>
/// Creates a horizontal separator line.
/// </summary>
protected Panel CreateSeparator()
{
return new Panel
{
Height = 1,
BackColor = BorderColor,
Dock = DockStyle.Top,
Margin = new Padding(0, 8, 0, 8)
};
}
/// <summary>
/// Creates a styled progress bar.
/// </summary>
protected ProgressBar CreateModernProgressBar()
{
return new ProgressBar
{
Style = ProgressBarStyle.Continuous,
Height = 6,
ForeColor = AccentColor
};
}
}

View File

@ -1,120 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@ -1,464 +0,0 @@
using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Runtime.InteropServices;
using System.Windows.Forms;
using Microsoft.Win32;
namespace OpenClawTray;
/// <summary>
/// Modern flyout menu with Windows 11 styling - dark/light mode, rounded corners, acrylic blur.
/// Replaces the dated ContextMenuStrip with a custom-drawn popup.
/// </summary>
public class ModernTrayMenu : Form
{
// DWM APIs for acrylic/mica effect
[DllImport("dwmapi.dll")]
private static extern int DwmSetWindowAttribute(IntPtr hwnd, int attr, ref int attrValue, int attrSize);
[DllImport("dwmapi.dll")]
private static extern int DwmExtendFrameIntoClientArea(IntPtr hWnd, ref MARGINS pMarInset);
[StructLayout(LayoutKind.Sequential)]
private struct MARGINS { public int Left, Right, Top, Bottom; }
private const int DWMWA_USE_IMMERSIVE_DARK_MODE = 20;
private const int DWMWA_WINDOW_CORNER_PREFERENCE = 33;
private const int DWMWA_SYSTEMBACKDROP_TYPE = 38;
private const int DWMWCP_ROUND = 2;
private const int DWMSBT_TRANSIENTWINDOW = 3; // Acrylic
// Theme colors
private bool _isDarkMode;
private Color _backgroundColor;
private Color _foregroundColor;
private Color _hoverColor;
private Color _accentColor;
private Color _separatorColor;
private Color _subtleTextColor;
// Menu items
private readonly List<ModernMenuItem> _items = new();
private int _hoveredIndex = -1;
private const int ItemHeight = 36;
private const int IconWidth = 32; // Wider for emoji
private const int Padding = 16; // More padding
private const int CornerRadius = 8;
private readonly ToolTip _toolTip = new() { InitialDelay = 400, ReshowDelay = 100 };
private int _lastTooltipIndex = -1;
public event EventHandler<string>? MenuItemClicked;
public ModernTrayMenu()
{
SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint | ControlStyles.DoubleBuffer, true);
FormBorderStyle = FormBorderStyle.None;
ShowInTaskbar = false;
TopMost = true;
StartPosition = FormStartPosition.Manual;
// Detect theme (styling applied in OnHandleCreated)
DetectTheme();
// Track mouse for hover effects
MouseMove += OnMouseMove;
MouseLeave += (_, _) => { _hoveredIndex = -1; Invalidate(); };
MouseClick += OnMouseClick;
// Close when clicking outside
Deactivate += (_, _) => Hide();
}
protected override void OnHandleCreated(EventArgs e)
{
base.OnHandleCreated(e);
ApplyModernStyling();
}
private void DetectTheme()
{
try
{
using var key = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize");
var value = key?.GetValue("AppsUseLightTheme");
_isDarkMode = value is int i && i == 0;
}
catch
{
_isDarkMode = false;
}
if (_isDarkMode)
{
_backgroundColor = Color.FromArgb(32, 32, 32);
_foregroundColor = Color.FromArgb(255, 255, 255);
_hoverColor = Color.FromArgb(45, 45, 48);
_accentColor = Color.FromArgb(255, 99, 71); // Lobster red
_separatorColor = Color.FromArgb(80, 80, 80);
_subtleTextColor = Color.FromArgb(180, 180, 180);
}
else
{
_backgroundColor = Color.FromArgb(249, 249, 249);
_foregroundColor = Color.FromArgb(28, 28, 28);
_hoverColor = Color.FromArgb(229, 229, 229);
_accentColor = Color.FromArgb(220, 53, 53); // Lobster red
_separatorColor = Color.FromArgb(200, 200, 200);
_subtleTextColor = Color.FromArgb(100, 100, 100);
}
BackColor = _backgroundColor;
}
private void ApplyModernStyling()
{
// Enable Windows 11 rounded corners
int preference = DWMWCP_ROUND;
DwmSetWindowAttribute(Handle, DWMWA_WINDOW_CORNER_PREFERENCE, ref preference, sizeof(int));
// Enable dark mode for title bar (affects some rendering)
int darkMode = _isDarkMode ? 1 : 0;
DwmSetWindowAttribute(Handle, DWMWA_USE_IMMERSIVE_DARK_MODE, ref darkMode, sizeof(int));
// Try to enable acrylic backdrop (Windows 11 22H2+)
int backdropType = DWMSBT_TRANSIENTWINDOW;
DwmSetWindowAttribute(Handle, DWMWA_SYSTEMBACKDROP_TYPE, ref backdropType, sizeof(int));
}
public void ClearItems() => _items.Clear();
public void AddBrandHeader(string icon, string text, string? tooltip = null)
{
_items.Add(new ModernMenuItem
{
Id = "",
Icon = icon,
Text = text,
Enabled = false,
IsHeader = true,
IsBrandHeader = true,
IsSeparator = false,
Tooltip = tooltip
});
}
public void AddItem(string id, string icon, string text, bool enabled = true, bool isHeader = false)
{
_items.Add(new ModernMenuItem
{
Id = id,
Icon = icon,
Text = text,
Enabled = enabled,
IsHeader = isHeader,
IsSeparator = false
});
}
public void AddSeparator()
{
_items.Add(new ModernMenuItem { IsSeparator = true });
}
public void AddStatusItem(string id, string icon, string text, string status, Color statusColor)
{
_items.Add(new ModernMenuItem
{
Id = id,
Icon = icon,
Text = text,
Status = status,
StatusColor = statusColor,
Enabled = true
});
}
public void ShowAtCursor()
{
// Calculate size
int width = 320; // Wider for better spacing
int height = Padding * 2;
foreach (var item in _items)
{
if (item.IsSeparator)
height += 9;
else if (item.IsBrandHeader)
height += 48; // Big brand header
else if (item.IsHeader)
height += 32;
else
height += ItemHeight;
}
// Minimum height if no items
if (height < 50) height = 50;
Size = new Size(width, height);
// Position near cursor, but keep on screen
var cursor = Cursor.Position;
var screen = Screen.FromPoint(cursor).WorkingArea;
int x = cursor.X - width / 2;
int y = cursor.Y - height - 10;
// Adjust if off screen
if (x < screen.Left) x = screen.Left + 8;
if (x + width > screen.Right) x = screen.Right - width - 8;
if (y < screen.Top) y = cursor.Y + 20; // Show below cursor instead
if (y + height > screen.Bottom) y = screen.Bottom - height - 8;
Location = new Point(x, y);
Show();
Activate();
Invalidate(); // Force repaint
}
protected override void OnPaint(PaintEventArgs e)
{
var g = e.Graphics;
g.SmoothingMode = SmoothingMode.AntiAlias;
g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.ClearTypeGridFit;
// Draw rounded background
using var bgBrush = new SolidBrush(_backgroundColor);
using var path = CreateRoundedRectangle(ClientRectangle, CornerRadius);
g.FillPath(bgBrush, path);
// Draw border
using var borderPen = new Pen(Color.FromArgb(_isDarkMode ? 50 : 30, _isDarkMode ? 255 : 0, _isDarkMode ? 255 : 0, _isDarkMode ? 255 : 0), 1);
g.DrawPath(borderPen, path);
// Draw items
int y = Padding;
for (int i = 0; i < _items.Count; i++)
{
var item = _items[i];
if (item.IsSeparator)
{
// Draw separator line
using var sepPen = new Pen(_separatorColor, 1);
g.DrawLine(sepPen, Padding, y + 4, Width - Padding, y + 4);
y += 9;
continue;
}
int itemHeight;
if (item.IsBrandHeader)
itemHeight = 48;
else if (item.IsHeader)
itemHeight = 32;
else
itemHeight = ItemHeight;
var itemRect = new Rectangle(8, y, Width - 16, itemHeight);
// Hover highlight
if (i == _hoveredIndex && item.Enabled && !item.IsHeader)
{
using var hoverBrush = new SolidBrush(_hoverColor);
using var hoverPath = CreateRoundedRectangle(itemRect, 4);
g.FillPath(hoverBrush, hoverPath);
}
// Icon - special handling for brand header
if (!string.IsNullOrEmpty(item.Icon))
{
Color iconColor;
float iconFontSize;
string fontName;
int iconWidth;
if (item.IsBrandHeader)
{
iconColor = _accentColor;
iconFontSize = 26; // Big lobster!
fontName = "Segoe UI Emoji"; // Use emoji font for lobster
iconWidth = 60; // Plenty of room for lobster
}
else if (item.IsHeader)
{
iconColor = _accentColor;
iconFontSize = 14;
fontName = "Segoe UI Symbol";
iconWidth = IconWidth;
}
else if (!item.Enabled || string.IsNullOrEmpty(item.Id) || item.Id.StartsWith("session:"))
{
iconColor = _subtleTextColor;
iconFontSize = 11;
fontName = "Segoe UI Symbol";
iconWidth = IconWidth;
}
else
{
iconColor = _accentColor;
iconFontSize = 11;
fontName = "Segoe UI Symbol";
iconWidth = IconWidth;
}
using var iconFont = new Font(fontName, iconFontSize);
var iconRect = new Rectangle(Padding, y, iconWidth, itemHeight);
TextRenderer.DrawText(g, item.Icon, iconFont, iconRect, iconColor,
TextFormatFlags.Left | TextFormatFlags.VerticalCenter);
}
// Text
var textColor = item.IsHeader ? _foregroundColor : (item.Enabled ? _foregroundColor : _subtleTextColor);
var fontSize = item.IsBrandHeader ? 14f : (item.IsHeader ? 10.5f : 9.5f);
var fontStyle = (item.IsHeader || item.IsBrandHeader) ? FontStyle.Bold : FontStyle.Regular;
using var textFont = new Font("Segoe UI", fontSize, fontStyle);
var textX = Padding + (item.IsBrandHeader ? 64 : IconWidth + 4);
// Only reserve space for status badge if item has one
var rightMargin = string.IsNullOrEmpty(item.Status) ? Padding : 70;
var textRect = new Rectangle(textX, y, Width - textX - rightMargin, itemHeight);
TextRenderer.DrawText(g, item.Text, textFont, textRect, textColor,
TextFormatFlags.Left | TextFormatFlags.VerticalCenter | TextFormatFlags.EndEllipsis);
// Status badge (right side)
if (!string.IsNullOrEmpty(item.Status))
{
using var statusFont = new Font("Segoe UI", 8, FontStyle.Bold);
var statusSize = TextRenderer.MeasureText(item.Status, statusFont);
var statusRect = new Rectangle(Width - Padding - statusSize.Width - 12, y + (itemHeight - 18) / 2, statusSize.Width + 8, 18);
using var statusBgBrush = new SolidBrush(Color.FromArgb(30, item.StatusColor));
using var statusPath = CreateRoundedRectangle(statusRect, 4);
g.FillPath(statusBgBrush, statusPath);
TextRenderer.DrawText(g, item.Status, statusFont, statusRect, item.StatusColor,
TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter);
}
y += itemHeight;
}
}
private void OnMouseMove(object? sender, MouseEventArgs e)
{
int y = Padding;
int newHover = -1;
int tooltipIndex = -1;
for (int i = 0; i < _items.Count; i++)
{
var item = _items[i];
int itemHeight;
if (item.IsSeparator)
itemHeight = 9;
else if (item.IsBrandHeader)
itemHeight = 48;
else if (item.IsHeader)
itemHeight = 32;
else
itemHeight = ItemHeight;
// Check if mouse is over this item
if (e.Y >= y && e.Y < y + itemHeight)
{
// Show tooltip for brand header
if (item.IsBrandHeader && !string.IsNullOrEmpty(item.Tooltip))
{
tooltipIndex = i;
}
}
// Allow hover on non-separators that are either:
// - Not headers and enabled, OR
// - Headers with an ID (clickable headers like Sessions)
var isClickable = !item.IsSeparator && !item.IsBrandHeader &&
((!item.IsHeader && item.Enabled) || (item.IsHeader && !string.IsNullOrEmpty(item.Id)));
if (isClickable)
{
if (e.Y >= y && e.Y < y + itemHeight)
{
newHover = i;
}
}
y += itemHeight;
}
// Update tooltip
if (tooltipIndex != _lastTooltipIndex)
{
_lastTooltipIndex = tooltipIndex;
if (tooltipIndex >= 0)
{
_toolTip.SetToolTip(this, _items[tooltipIndex].Tooltip);
}
else
{
_toolTip.SetToolTip(this, null);
}
}
if (newHover != _hoveredIndex)
{
_hoveredIndex = newHover;
Cursor = newHover >= 0 ? Cursors.Hand : Cursors.Default;
Invalidate();
}
}
private void OnMouseClick(object? sender, MouseEventArgs e)
{
if (_hoveredIndex >= 0 && _hoveredIndex < _items.Count)
{
var item = _items[_hoveredIndex];
// Allow clicking if enabled, not separator, and either not a header OR a header with an ID
if (item.Enabled && !item.IsSeparator && (!item.IsHeader || !string.IsNullOrEmpty(item.Id)))
{
Hide();
MenuItemClicked?.Invoke(this, item.Id);
}
}
}
private static GraphicsPath CreateRoundedRectangle(Rectangle rect, int radius)
{
var path = new GraphicsPath();
int diameter = radius * 2;
var arc = new Rectangle(rect.X, rect.Y, diameter, diameter);
path.AddArc(arc, 180, 90); // Top-left
arc.X = rect.Right - diameter;
path.AddArc(arc, 270, 90); // Top-right
arc.Y = rect.Bottom - diameter;
path.AddArc(arc, 0, 90); // Bottom-right
arc.X = rect.Left;
path.AddArc(arc, 90, 90); // Bottom-left
path.CloseFigure();
return path;
}
protected override CreateParams CreateParams
{
get
{
var cp = base.CreateParams;
cp.ClassStyle |= 0x00020000; // CS_DROPSHADOW
return cp;
}
}
private class ModernMenuItem
{
public string Id { get; set; } = "";
public string Icon { get; set; } = "";
public string Text { get; set; } = "";
public string Status { get; set; } = "";
public Color StatusColor { get; set; } = Color.Gray;
public bool Enabled { get; set; } = true;
public bool IsSeparator { get; set; }
public bool IsHeader { get; set; }
public bool IsBrandHeader { get; set; }
public string? Tooltip { get; set; }
}
}

View File

@ -1,166 +0,0 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Windows.Forms;
namespace OpenClawTray;
/// <summary>
/// Shows recent notification history in a modern styled list view.
/// </summary>
public class NotificationHistoryForm : ModernForm
{
private ListView? _listView;
private Button _clearButton = null!;
private Button _closeButton = null!;
private static NotificationHistoryForm? _instance;
private static readonly List<NotificationEntry> _history = new();
private const int MaxHistory = 200;
public static void AddEntry(string title, string message, string type)
{
lock (_history)
{
_history.Add(new NotificationEntry
{
Timestamp = DateTime.Now,
Title = title,
Message = message,
Type = type
});
while (_history.Count > MaxHistory)
_history.RemoveAt(0);
}
_instance?.RefreshList();
}
public static void ShowOrFocus()
{
if (_instance != null && !_instance.IsDisposed)
{
_instance.BringToFront();
_instance.Focus();
return;
}
_instance = new NotificationHistoryForm();
_instance.Show();
}
private NotificationHistoryForm()
{
InitializeComponent();
RefreshList();
}
private void InitializeComponent()
{
Text = "Notification History — OpenClaw Tray";
Size = new Size(680, 500);
MinimumSize = new Size(480, 340);
FormBorderStyle = FormBorderStyle.Sizable;
Icon = IconHelper.GetLobsterIcon();
_listView = new ListView
{
Dock = DockStyle.Fill,
View = View.Details,
FullRowSelect = true,
GridLines = false,
Font = new Font("Segoe UI", 9.5F),
BackColor = SurfaceColor,
ForeColor = ForegroundColor,
BorderStyle = BorderStyle.None
};
_listView.Columns.Add("Time", 140);
_listView.Columns.Add("Type", 85);
_listView.Columns.Add("Title", 160);
_listView.Columns.Add("Message", 320);
var buttonPanel = new Panel
{
Dock = DockStyle.Bottom,
Height = 56,
BackColor = SurfaceColor,
Padding = new Padding(16, 12, 16, 12)
};
_closeButton = CreateModernButton("Close");
_closeButton.Size = new Size(90, 36);
_closeButton.Click += (_, _) => Close();
_clearButton = CreateModernButton("Clear All", isPrimary: true);
_clearButton.Size = new Size(100, 36);
_clearButton.Click += (_, _) =>
{
lock (_history) _history.Clear();
RefreshList();
};
var buttonFlow = new FlowLayoutPanel
{
Dock = DockStyle.Right,
FlowDirection = FlowDirection.RightToLeft,
AutoSize = true,
BackColor = Color.Transparent
};
buttonFlow.Controls.Add(_closeButton);
buttonFlow.Controls.Add(_clearButton);
buttonPanel.Controls.Add(buttonFlow);
buttonPanel.Controls.Add(_closeButton);
buttonPanel.Controls.Add(_clearButton);
Controls.Add(_listView);
Controls.Add(buttonPanel);
}
private void RefreshList()
{
if (_listView == null || _listView.IsDisposed) return;
if (InvokeRequired)
{
Invoke(new Action(RefreshList));
return;
}
_listView.BeginUpdate();
_listView.Items.Clear();
lock (_history)
{
for (int i = _history.Count - 1; i >= 0; i--)
{
var entry = _history[i];
var item = new ListViewItem(entry.Timestamp.ToString("yyyy-MM-dd HH:mm:ss"));
item.SubItems.Add(entry.Type);
item.SubItems.Add(entry.Title);
item.SubItems.Add(entry.Message.Replace('\n', ' '));
_listView.Items.Add(item);
}
}
_listView.EndUpdate();
}
protected override void OnFormClosed(FormClosedEventArgs e)
{
_instance = null;
base.OnFormClosed(e);
}
private class NotificationEntry
{
public DateTime Timestamp { get; set; }
public string Title { get; set; } = "";
public string Message { get; set; } = "";
public string Type { get; set; } = "";
}
}

View File

@ -1,39 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net10.0-windows10.0.19041.0</TargetFramework>
<UseWindowsForms>true</UseWindowsForms>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<EnableWindowsTargeting>true</EnableWindowsTargeting>
<NoWarn>MSB3277</NoWarn>
<ApplicationIcon>openclaw.ico</ApplicationIcon>
<AssemblyTitle>OpenClaw Windows Tray</AssemblyTitle>
<AssemblyDescription>OpenClaw</AssemblyDescription>
<AssemblyCompany>Scott Hanselman</AssemblyCompany>
<AssemblyProduct>OpenClaw Tray</AssemblyProduct>
<Copyright>Copyright © 2026 Scott Hanselman</Copyright>
<Version>0.4.4</Version>
<PublishSingleFile>true</PublishSingleFile>
<SelfContained>true</SelfContained>
<!-- RuntimeIdentifier set at publish time: win-x64 or win-arm64 -->
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\OpenClaw.Shared\OpenClaw.Shared.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Toolkit.Uwp.Notifications" Version="7.1.3" />
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.3124.44" />
<PackageReference Include="System.Text.Json" Version="9.0.0" />
<PackageReference Include="Updatum" Version="1.3.4" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Icons\*.ico" />
</ItemGroup>
</Project>

View File

@ -1,205 +0,0 @@
using OpenClawTray;
using System;
using System.Diagnostics;
using System.IO.Pipes;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using Updatum;
namespace OpenClawTray;
internal static class Program
{
private const string PipeName = "OpenClawTray-DeepLink";
internal static readonly UpdatumManager AppUpdater = new("shanselman", "openclaw-windows-hub")
{
FetchOnlyLatestRelease = true,
InstallUpdateSingleFileExecutableName = "OpenClaw.Tray",
};
[STAThread]
static void Main(string[] args)
{
// Single instance check
using var mutex = new Mutex(true, "OpenClawTray", out bool createdNew);
if (!createdNew)
{
// Forward deep link args to running instance via named pipe
if (args.Length > 0 && args[0].StartsWith("openclaw://", StringComparison.OrdinalIgnoreCase))
{
SendDeepLinkToRunningInstance(args[0]);
}
else
{
MessageBox.Show("OpenClaw Tray is already running.", "OpenClaw Tray",
MessageBoxButtons.OK, MessageBoxIcon.Information);
}
return;
}
// Register URI scheme on first run
DeepLinkHandler.RegisterUriScheme();
Application.SetHighDpiMode(HighDpiMode.SystemAware);
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
// Check for updates before launching
var shouldLaunch = CheckForUpdatesAsync().GetAwaiter().GetResult();
if (shouldLaunch)
{
var trayApp = new TrayApplication(args);
Application.Run(trayApp);
}
}
private static async Task<bool> CheckForUpdatesAsync()
{
try
{
Logger.Info("Checking for updates...");
var updateFound = await AppUpdater.CheckForUpdatesAsync();
if (!updateFound)
{
Logger.Info("No updates available");
return true;
}
var release = AppUpdater.LatestRelease!;
var changelog = AppUpdater.GetChangelog(true) ?? "No release notes available.";
Logger.Info($"Update available: {release.TagName}");
using var dialog = new UpdateDialog(release.TagName, changelog);
dialog.ShowDialog();
if (dialog.Result == UpdateDialogResult.Download)
{
var installed = await DownloadAndInstallUpdateAsync();
// If install succeeded, app will restart - don't launch
// If install failed, let user continue to the app
return !installed;
}
// RemindLater or Skip - continue to launch
return true;
}
catch (Exception ex)
{
// Update check failed - don't block the app, just launch
Logger.Warn($"Update check failed: {ex.Message}");
return true;
}
}
private static async Task<bool> DownloadAndInstallUpdateAsync()
{
DownloadProgressDialog? progressDialog = null;
try
{
progressDialog = new DownloadProgressDialog(AppUpdater);
progressDialog.Show();
var downloadedAsset = await AppUpdater.DownloadUpdateAsync();
progressDialog?.Close();
progressDialog = null;
if (downloadedAsset == null)
{
MessageBox.Show("Failed to download the update. Please try again later.",
"Download Failed", MessageBoxButtons.OK, MessageBoxIcon.Error);
return false;
}
if (!System.IO.File.Exists(downloadedAsset.FilePath))
{
MessageBox.Show($"Update file was deleted or is inaccessible:\n{downloadedAsset.FilePath}\n\nThis may be caused by antivirus software.",
"Update File Missing", MessageBoxButtons.OK, MessageBoxIcon.Error);
return false;
}
var confirmResult = MessageBox.Show(
"The update has been downloaded. OpenClaw Tray will now restart to install the update.\n\nContinue?",
"Install Update",
MessageBoxButtons.YesNo,
MessageBoxIcon.Question);
if (confirmResult == DialogResult.Yes)
{
Logger.Info("Installing update and restarting...");
await AppUpdater.InstallUpdateAsync(downloadedAsset);
return true; // App will restart
}
return false; // User cancelled, continue to app
}
catch (UnauthorizedAccessException ex)
{
MessageBox.Show($"Access denied when accessing update file.\n\n1. Antivirus may be blocking the update\n2. Windows SmartScreen may need approval\n\nError: {ex.Message}",
"Access Denied", MessageBoxButtons.OK, MessageBoxIcon.Error);
return false;
}
catch (Exception ex)
{
Logger.Error($"Update download/install failed: {ex.Message}");
MessageBox.Show($"Failed to download or install update: {ex.Message}",
"Update Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
return false;
}
finally
{
progressDialog?.Close();
}
}
private static void SendDeepLinkToRunningInstance(string uri)
{
try
{
using var pipe = new NamedPipeClientStream(".", PipeName, PipeDirection.Out);
pipe.Connect(1000); // 1 second timeout
using var writer = new System.IO.StreamWriter(pipe);
writer.WriteLine(uri);
writer.Flush();
Logger.Info($"Forwarded deep link to running instance: {uri}");
}
catch (Exception ex)
{
Logger.Warn($"Failed to forward deep link: {ex.Message}");
MessageBox.Show($"OpenClaw Tray is running but couldn't process the deep link.\n\nPlease try again.",
"Deep Link Error", MessageBoxButtons.OK, MessageBoxIcon.Warning);
}
}
internal static void StartDeepLinkServer(Action<string> onDeepLinkReceived)
{
Task.Run(async () =>
{
while (true)
{
try
{
using var pipe = new NamedPipeServerStream(PipeName, PipeDirection.In);
await pipe.WaitForConnectionAsync();
using var reader = new System.IO.StreamReader(pipe);
var uri = await reader.ReadLineAsync();
if (!string.IsNullOrEmpty(uri))
{
Logger.Info($"Received deep link via IPC: {uri}");
onDeepLinkReceived(uri);
}
}
catch (Exception ex)
{
Logger.Warn($"Deep link server error: {ex.Message}");
await Task.Delay(1000);
}
}
});
}
}

View File

@ -1,105 +0,0 @@
using System;
using System.Drawing;
using System.Windows.Forms;
namespace OpenClawTray;
public partial class QuickSendDialog : ModernForm
{
private TextBox _messageTextBox = null!;
private Button _sendButton = null!;
private Button _cancelButton = null!;
private Label _hintLabel = null!;
public string Message => _messageTextBox.Text;
public QuickSendDialog()
{
InitializeComponent();
}
private void InitializeComponent()
{
Text = "Quick Send — OpenClaw";
Size = new Size(520, 300);
ShowInTaskbar = true;
TopMost = true;
Icon = IconHelper.GetLobsterIcon();
// Header label
var label = CreateModernLabel("Send a message to OpenClaw:");
label.Location = new Point(20, 20);
label.Font = new Font("Segoe UI", 11F, FontStyle.Bold);
label.ForeColor = AccentColor;
// Message text box
_messageTextBox = CreateModernTextBox();
_messageTextBox.Location = new Point(20, 52);
_messageTextBox.Size = new Size(464, 110);
_messageTextBox.Multiline = true;
_messageTextBox.ScrollBars = ScrollBars.Vertical;
_messageTextBox.AcceptsReturn = false;
_messageTextBox.Font = new Font("Segoe UI", 10.5f);
// Buttons row (below text box)
_sendButton = CreateModernButton("Send", isPrimary: true);
_sendButton.Location = new Point(394, 172);
_sendButton.Size = new Size(90, 32);
_sendButton.Click += OnSendClick;
_cancelButton = CreateModernButton("Cancel");
_cancelButton.Location = new Point(296, 172);
_cancelButton.Size = new Size(90, 32);
_cancelButton.Click += OnCancelClick;
// Hint label (below buttons with more space)
_hintLabel = CreateModernLabel("Enter to send · Esc to cancel · Shift+Enter for new line", isSubtle: true);
_hintLabel.Location = new Point(20, 220);
_hintLabel.Font = new Font("Segoe UI", 8.5F);
AcceptButton = _sendButton;
CancelButton = _cancelButton;
Controls.AddRange(new Control[] { label, _messageTextBox, _sendButton, _cancelButton, _hintLabel });
Shown += (_, _) =>
{
_messageTextBox.Focus();
Activate();
};
}
private void OnSendClick(object? sender, EventArgs e)
{
if (string.IsNullOrWhiteSpace(_messageTextBox.Text))
{
_messageTextBox.Focus();
return;
}
DialogResult = DialogResult.OK;
Close();
}
private void OnCancelClick(object? sender, EventArgs e)
{
DialogResult = DialogResult.Cancel;
Close();
}
public void SetMessage(string message)
{
_messageTextBox.Text = message;
_messageTextBox.SelectionStart = message.Length;
}
protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
{
if (keyData == (Keys.Control | Keys.Enter) || keyData == Keys.Enter)
{
OnSendClick(null, EventArgs.Empty);
return true;
}
return base.ProcessCmdKey(ref msg, keyData);
}
}

View File

@ -1,228 +0,0 @@
# OpenClaw Windows Tray
A Windows system tray companion for [OpenClaw](https://openclaw.ai) — the Windows equivalent of the macOS menu bar app. Provides desktop notifications, embedded chat, live agent activity monitoring, and gateway status tracking.
## Features
### System Tray
- **Lobster icon** 🦞 when connected (pixel art), color-coded circles for other states
- **Activity badge** showing what the agent is doing (exec, read, write, edit, search, browser, message, tool)
- **Context menu** with status, sessions, channels, usage, quick send, settings, and auto-start
- **Text status labels** `[ON]`/`[OFF]`/`[READY]`/`[LINKED]` for clarity
- **Clickable status** opens detailed status view
- **Double-click** opens embedded web chat
- **Open Dashboard** opens browser with authenticated session
### Session Awareness
- **Live session tracking** — see main and sub-sessions in real-time
- **Session detail** — model, channel, current activity per session
- **Activity display** — "Main · 💻 pnpm test" or "Sub · 📄 reading file"
### Usage & Context
- **Token usage** display (input/output/total with human-readable formatting)
- **Cost tracking** when available from gateway
- **Request count** and active model
### WebChat Panel
- **Embedded chat** via WebView2 — no browser needed
- **Dark mode** background
- Toolbar with home, refresh, pop-out to browser, and DevTools
- Singleton window (double-click tray icon or "Open Web Chat" menu)
### Notifications
- **Windows toast notifications** with per-type filtering:
- 🩸 Health / CGM alerts
- 🚨 Urgent / error alerts
- ⏰ Reminders
- 📧 Email notifications
- 📅 Calendar events
- 🔨 Build / CI
- 📦 Stock availability
- 🤖 General info
- **Clickable toasts** — Quick Send toasts open dashboard when clicked
- **Notification history** — scrollable list with timestamps, even for filtered-out notifications
- Fallback to balloon tips if toast fails
### Channel Health
- Live WhatsApp, Telegram, and other channel status
- Smart status detection: `[READY]` (probe OK), `[LINKED]` (authenticated), `[ON]`/`[OFF]`
- Shows linked state, auth age, errors, and stale warnings
- On-demand health check button
### Keyboard Shortcuts
- **Ctrl+Alt+Shift+C** — Global hotkey to open Quick Send from anywhere
- **Enter** — Send message in Quick Send dialog
- **Shift+Enter** — New line in Quick Send
- **Esc** — Cancel Quick Send
### Deep Links
- Registers `openclaw://` URI scheme
- `openclaw://agent?message=Hello` sends a message to the agent
- Confirmation prompt for safety (bypass with `key` parameter)
### Quality of Life
- **ARM64 support** — native builds for Windows on ARM
- **Auto-start** via Windows Registry
- **Exponential backoff** on reconnect (1s → 60s)
- **File logging** to `%LOCALAPPDATA%\OpenClawTray\openclaw-tray.log` (with rotation at 1MB)
- **Open Log File** menu item for quick debugging
- **Single instance** enforcement (mutex)
- **Proper GDI handle cleanup** (no icon leaks)
- **Status detail view** — rich dark-themed status panel
## Requirements
- Windows 10 version 1903+ (for toast notifications)
- .NET 10 Runtime (included in self-contained builds)
- [WebView2 Runtime](https://developer.microsoft.com/en-us/microsoft-edge/webview2/) (for chat panel)
- OpenClaw gateway running (typically in WSL2)
## Quick Start
1. Download the latest release from [Releases](https://github.com/shanselman/moltbot-windows-hub/releases)
- **x64**: For Intel/AMD processors
- **arm64**: For Windows on ARM (e.g., Surface Pro X, Snapdragon laptops)
2. Run `OpenClawTray.exe`
3. Right-click tray icon → Settings
4. Enter gateway URL (`ws://localhost:18789`) and your token
5. Done — you'll see the icon turn green when connected
### Finding Your Gateway Token
```bash
# In WSL2:
cat ~/.OpenClaw/OpenClaw.json | grep token
# Or:
OpenClaw config get gateway.auth.token
```
## Build from Source
```bash
git clone https://github.com/shanselman/moltbot-windows-hub.git
cd moltbot-windows-hub
# Windows — auto-detects architecture
build.bat
# Manual build
dotnet build -c Release -r win-x64
dotnet build -c Release -r win-arm64
# Self-contained single-file executable
dotnet publish -c Release -r win-x64 --self-contained -p:PublishSingleFile=true -o publish
dotnet publish -c Release -r win-arm64 --self-contained -p:PublishSingleFile=true -o publish-arm64
# Cross-compile from Linux (for CI)
dotnet build -p:EnableWindowsTargeting=true -r win-x64
```
## Project Structure
```
├── Program.cs # Entry point, single instance, deep link registration
├── TrayApplication.cs # Tray icon, menu, event wiring, UI updates
├── OpenClawGatewayClient.cs # WebSocket client, protocol v3, event parsing, state tracking
├── WebChatForm.cs # WebView2 chat panel (singleton, dark mode)
├── QuickSendDialog.cs # Quick message input (Enter to send, TopMost)
├── StatusDetailForm.cs # Rich status detail view (dark theme)
├── NotificationHistoryForm.cs # Scrollable notification history
├── GlobalHotkey.cs # Ctrl+Alt+Shift+C system-wide hotkey
├── DeepLinkHandler.cs # openclaw:// URI scheme handler
├── SettingsManager.cs # JSON config with notification filters
├── SettingsDialog.cs # Settings UI (connection, startup, notification filters)
├── AutoStartManager.cs # Windows Registry auto-start
├── Logger.cs # File + debug logger with rotation
└── OpenClawTray.csproj # .NET 10, Windows Forms, WebView2
```
## macOS Parity Status
This Windows tray app aims for feature parity with the [OpenClaw macOS menu bar app](https://openclaw.ai-macos).
| Feature | macOS | Windows | Notes |
|---------|:-----:|:-------:|-------|
| System tray/menu bar icon | ✅ | ✅ | Lobster 🦞 when connected |
| Status colors/indicators | ✅ | ✅ | Text labels `[ON]/[OFF]` for clarity |
| Activity badges | ✅ | ✅ | exec/read/write/search/browser |
| Toast/native notifications | ✅ | ✅ | Windows toast + fallback |
| Per-type notification filters | ✅ | ✅ | Health, urgent, email, etc. |
| Clickable notifications | ✅ | ✅ | Opens dashboard with auth |
| Notification history | — | ✅ | Windows-only feature |
| Embedded chat (WebView) | ✅ | ✅ | WebView2 |
| Open Dashboard in browser | ✅ | ✅ | Token auto-included |
| Channel health display | ✅ | ✅ | Telegram, WhatsApp status |
| Session awareness (main/sub) | ✅ | ✅ | Live session tracking |
| Usage/token display | ✅ | ✅ | Input/output/total |
| Deep link URI scheme | ✅ | ✅ | `openclaw://` |
| Global hotkey | — | ✅ | Ctrl+Alt+Shift+C |
| Auto-start | ✅ | ✅ | Registry-based |
| Quick send | ✅ | ✅ | Fire-and-forget to main session |
| Health check (on-demand) | ✅ | ✅ | |
| Status detail view | — | ✅ | Windows-only feature |
| File logging | ✅ | ✅ | With rotation |
| ARM64 support | ✅ | ✅ | Apple Silicon / Windows ARM |
| Canvas panel | ✅ | 🔜 | Planned |
| Voice wake / push-to-talk | ✅ | 🔜 | Planned |
| Skills settings UI | ✅ | 🔜 | Planned |
| TCC permissions management | ✅ | N/A | macOS-specific |
| PeekabooBridge (UI automation) | ✅ | N/A | macOS-specific |
| XPC / node host service | ✅ | N/A | macOS-specific |
## Settings
Settings are stored in `%APPDATA%\OpenClawTray\settings.json`:
```json
{
"GatewayUrl": "ws://localhost:18789",
"Token": "your-token",
"AutoStart": false,
"ShowNotifications": true,
"NotificationSound": "Default",
"NotifyHealth": true,
"NotifyUrgent": true,
"NotifyReminder": true,
"NotifyEmail": true,
"NotifyCalendar": true,
"NotifyBuild": true,
"NotifyStock": true,
"NotifyInfo": true,
"ShowGlobalHotkey": true
}
```
## Troubleshooting
**Can't connect?**
- Check gateway: `OpenClaw gateway status` in WSL2
- Verify token matches `~/.OpenClaw/OpenClaw.json`
- Try WSL2 IP directly: `ws://<wsl-ip>:18789` (`wsl hostname -I`)
**No notifications?**
- Check Windows Settings → Notifications
- Check Focus Assist / Do Not Disturb
- Check notification filter settings in the app
**WebChat blank?**
- Install [WebView2 Runtime](https://developer.microsoft.com/en-us/microsoft-edge/webview2/)
- Check logs: `%LOCALAPPDATA%\OpenClawTray\openclaw-tray.log`
- Right-click tray → Open Log File
**Global hotkey not working?**
- Another app may have registered Ctrl+Alt+Shift+C
- Check Settings → Global hotkey is enabled
- Check the log file for "Failed to register global hotkey"
## License
MIT
## Credits
- Built with .NET 10, Windows Forms, and [WebView2](https://developer.microsoft.com/en-us/microsoft-edge/webview2/)
- Toast notifications via [Microsoft.Toolkit.Uwp.Notifications](https://github.com/CommunityToolkit/WindowsCommunityToolkit)
- Part of the [OpenClaw](https://openclaw.ai) ecosystem

View File

@ -1,338 +0,0 @@
using Microsoft.Toolkit.Uwp.Notifications;
using OpenClaw.Shared;
using System;
using System.Drawing;
using System.Windows.Forms;
namespace OpenClawTray;
public partial class SettingsDialog : ModernForm
{
private readonly SettingsManager _settings;
private TextBox _gatewayUrlTextBox = null!;
private TextBox _tokenTextBox = null!;
private CheckBox _autoStartCheckBox = null!;
private CheckBox _showNotificationsCheckBox = null!;
private CheckBox _globalHotkeyCheckBox = null!;
private ComboBox _notificationSoundComboBox = null!;
private Button _testConnectionButton = null!;
private Button _testNotificationButton = null!;
private Button _okButton = null!;
private Button _cancelButton = null!;
private Label _statusLabel = null!;
// Notification filter checkboxes
private CheckBox _notifyHealthCb = null!;
private CheckBox _notifyUrgentCb = null!;
private CheckBox _notifyReminderCb = null!;
private CheckBox _notifyEmailCb = null!;
private CheckBox _notifyCalendarCb = null!;
private CheckBox _notifyBuildCb = null!;
private CheckBox _notifyStockCb = null!;
private CheckBox _notifyInfoCb = null!;
private Panel _notifyFilterPanel = null!;
public SettingsDialog(SettingsManager settings)
{
_settings = settings;
InitializeComponent();
LoadSettings();
}
private void InitializeComponent()
{
Text = "Settings — OpenClaw Tray";
Size = new Size(480, 600);
ShowInTaskbar = false;
AutoScroll = false;
Icon = IconHelper.GetLobsterIcon();
var y = 16;
// --- Connection Section ---
var connHeader = CreateModernLabel("CONNECTION");
connHeader.Font = new Font("Segoe UI", 9F, FontStyle.Bold);
connHeader.ForeColor = AccentColor;
connHeader.Location = new Point(16, y);
y += 26;
var gatewayUrlLabel = CreateModernLabel("Gateway URL:");
gatewayUrlLabel.Location = new Point(16, y);
y += 24;
_gatewayUrlTextBox = CreateModernTextBox();
_gatewayUrlTextBox.Location = new Point(16, y);
_gatewayUrlTextBox.Size = new Size(310, 28);
_testConnectionButton = CreateModernButton("Test");
_testConnectionButton.Location = new Point(334, y - 2);
_testConnectionButton.Size = new Size(70, 30);
_testConnectionButton.Click += OnTestConnection;
y += 36;
var tokenLabel = CreateModernLabel("Token:");
tokenLabel.Location = new Point(16, y);
y += 24;
_tokenTextBox = CreateModernTextBox();
_tokenTextBox.Location = new Point(16, y);
_tokenTextBox.Size = new Size(310, 28);
_tokenTextBox.UseSystemPasswordChar = true;
_statusLabel = CreateModernLabel("", isSubtle: true);
_statusLabel.Location = new Point(334, y + 4);
_statusLabel.Font = new Font("Segoe UI", 8.5F);
y += 44;
// --- Startup Section ---
var startupHeader = CreateModernLabel("STARTUP");
startupHeader.Font = new Font("Segoe UI", 9F, FontStyle.Bold);
startupHeader.ForeColor = AccentColor;
startupHeader.Location = new Point(16, y);
y += 26;
_autoStartCheckBox = CreateModernCheckBox("Start automatically with Windows");
_autoStartCheckBox.Location = new Point(16, y);
y += 28;
_globalHotkeyCheckBox = CreateModernCheckBox("Global hotkey (Ctrl+Alt+Shift+C → Quick Send)");
_globalHotkeyCheckBox.Location = new Point(16, y);
y += 40;
// --- Notifications Section ---
var notifyHeader = CreateModernLabel("NOTIFICATIONS");
notifyHeader.Font = new Font("Segoe UI", 9F, FontStyle.Bold);
notifyHeader.ForeColor = AccentColor;
notifyHeader.Location = new Point(16, y);
y += 26;
_showNotificationsCheckBox = CreateModernCheckBox("Show desktop notifications");
_showNotificationsCheckBox.Location = new Point(16, y);
_showNotificationsCheckBox.CheckedChanged += (_, _) =>
{
_notifyFilterPanel.Enabled = _showNotificationsCheckBox.Checked;
};
y += 28;
var soundLabel = CreateModernLabel("Sound:");
soundLabel.Location = new Point(16, y + 3);
soundLabel.AutoSize = true;
_notificationSoundComboBox = new ComboBox
{
Location = new Point(80, y),
Size = new Size(140, 28),
DropDownStyle = ComboBoxStyle.DropDownList,
Font = new Font("Segoe UI", 9.5f),
BackColor = SurfaceColor,
ForeColor = ForegroundColor,
FlatStyle = FlatStyle.Flat
};
// Items are stable persistence keys (must match values in settings.json).
// Do not localize these strings — use a display/key mapping if localization is needed.
_notificationSoundComboBox.Items.AddRange(new[] { "Default", "None", "Critical", "Information" });
_testNotificationButton = CreateModernButton("Test");
_testNotificationButton.Location = new Point(230, y);
_testNotificationButton.Size = new Size(80, 28);
_testNotificationButton.Click += OnTestNotification;
y += 36;
// Filter panel
var filterLabel = CreateModernLabel("Show toasts for:", isSubtle: true);
filterLabel.Location = new Point(16, y);
y += 24;
_notifyFilterPanel = new Panel
{
Location = new Point(16, y),
Size = new Size(440, 72),
BorderStyle = BorderStyle.None,
BackColor = Color.Transparent
};
// Two columns of filter checkboxes
_notifyHealthCb = MakeFilterCb("🩸 Health", 0, 0);
_notifyUrgentCb = MakeFilterCb("🚨 Urgent", 0, 24);
_notifyReminderCb = MakeFilterCb("⏰ Reminders", 0, 48);
_notifyEmailCb = MakeFilterCb("📧 Email", 150, 0);
_notifyCalendarCb = MakeFilterCb("📅 Calendar", 150, 24);
_notifyBuildCb = MakeFilterCb("🔨 Build/CI", 150, 48);
_notifyStockCb = MakeFilterCb("📦 Stock", 300, 0);
_notifyInfoCb = MakeFilterCb("🤖 General", 300, 24);
_notifyFilterPanel.Controls.AddRange(new Control[]
{
_notifyHealthCb, _notifyUrgentCb, _notifyReminderCb,
_notifyEmailCb, _notifyCalendarCb, _notifyBuildCb,
_notifyStockCb, _notifyInfoCb
});
y += 90;
// --- About Section ---
var aboutLabel = CreateModernLabel("Made with 🦞 love by Scott Hanselman and Molty", isSubtle: true);
aboutLabel.Font = new Font("Segoe UI", 8.5F);
aboutLabel.Location = new Point(16, y);
aboutLabel.AutoSize = true;
aboutLabel.Cursor = Cursors.Hand;
aboutLabel.Click += (_, _) => System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
{
FileName = "https://github.com/shanselman/openclaw-windows-hub",
UseShellExecute = true
});
y += 30;
// --- Buttons ---
_cancelButton = CreateModernButton("Cancel");
_cancelButton.Location = new Point(Width - 116, y);
_cancelButton.Size = new Size(90, 34);
_cancelButton.Click += OnCancelClick;
_okButton = CreateModernButton("Save", isPrimary: true);
_okButton.Location = new Point(Width - 214, y);
_okButton.Size = new Size(90, 34);
_okButton.Click += OnOkClick;
AcceptButton = _okButton;
CancelButton = _cancelButton;
// Add all controls
Controls.AddRange(new Control[]
{
connHeader, gatewayUrlLabel, _gatewayUrlTextBox, _testConnectionButton,
tokenLabel, _tokenTextBox, _statusLabel,
startupHeader, _autoStartCheckBox, _globalHotkeyCheckBox,
notifyHeader, _showNotificationsCheckBox, soundLabel, _notificationSoundComboBox, _testNotificationButton,
filterLabel, _notifyFilterPanel,
aboutLabel, _okButton, _cancelButton
});
}
private void OnTestNotification(object? sender, EventArgs e)
{
try
{
new ToastContentBuilder()
.AddText("🦞 Test Notification")
.AddText("This is what an OpenClaw notification looks like!")
.Show();
}
catch
{
MessageBox.Show("Notifications may not be available on this system.", "Test", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
}
private CheckBox MakeFilterCb(string text, int x, int y)
{
var cb = CreateModernCheckBox(text);
cb.Location = new Point(x, y);
cb.Size = new Size(140, 22);
cb.Font = new Font("Segoe UI", 8.5F);
cb.Checked = true;
return cb;
}
private void LoadSettings()
{
_gatewayUrlTextBox.Text = _settings.GatewayUrl;
_tokenTextBox.Text = _settings.Token;
_autoStartCheckBox.Checked = _settings.AutoStart;
_globalHotkeyCheckBox.Checked = _settings.ShowGlobalHotkey;
_showNotificationsCheckBox.Checked = _settings.ShowNotifications;
_notifyFilterPanel.Enabled = _settings.ShowNotifications;
var soundIndex = _notificationSoundComboBox.Items.IndexOf(_settings.NotificationSound);
_notificationSoundComboBox.SelectedIndex = soundIndex >= 0 ? soundIndex : 0;
_notifyHealthCb.Checked = _settings.NotifyHealth;
_notifyUrgentCb.Checked = _settings.NotifyUrgent;
_notifyReminderCb.Checked = _settings.NotifyReminder;
_notifyEmailCb.Checked = _settings.NotifyEmail;
_notifyCalendarCb.Checked = _settings.NotifyCalendar;
_notifyBuildCb.Checked = _settings.NotifyBuild;
_notifyStockCb.Checked = _settings.NotifyStock;
_notifyInfoCb.Checked = _settings.NotifyInfo;
}
private void SaveSettings()
{
_settings.GatewayUrl = _gatewayUrlTextBox.Text.Trim();
_settings.Token = _tokenTextBox.Text.Trim();
_settings.AutoStart = _autoStartCheckBox.Checked;
_settings.ShowGlobalHotkey = _globalHotkeyCheckBox.Checked;
_settings.ShowNotifications = _showNotificationsCheckBox.Checked;
_settings.NotificationSound = _notificationSoundComboBox.SelectedItem?.ToString() ?? "Default";
_settings.NotifyHealth = _notifyHealthCb.Checked;
_settings.NotifyUrgent = _notifyUrgentCb.Checked;
_settings.NotifyReminder = _notifyReminderCb.Checked;
_settings.NotifyEmail = _notifyEmailCb.Checked;
_settings.NotifyCalendar = _notifyCalendarCb.Checked;
_settings.NotifyBuild = _notifyBuildCb.Checked;
_settings.NotifyStock = _notifyStockCb.Checked;
_settings.NotifyInfo = _notifyInfoCb.Checked;
}
private async void OnTestConnection(object? sender, EventArgs e)
{
_testConnectionButton.Enabled = false;
_statusLabel.Text = "Testing...";
_statusLabel.ForeColor = Color.Blue;
try
{
var testClient = new OpenClawGatewayClient(
_gatewayUrlTextBox.Text.Trim(),
_tokenTextBox.Text.Trim());
await testClient.ConnectAsync();
await testClient.DisconnectAsync();
testClient.Dispose();
_statusLabel.Text = "✅ Connected";
_statusLabel.ForeColor = Color.DarkGreen;
}
catch (Exception ex)
{
_statusLabel.Text = $"❌ {ex.Message}";
_statusLabel.ForeColor = Color.Red;
}
finally
{
_testConnectionButton.Enabled = true;
}
}
private void OnOkClick(object? sender, EventArgs e)
{
if (string.IsNullOrWhiteSpace(_gatewayUrlTextBox.Text))
{
MessageBox.Show("Gateway URL is required.", "Settings",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
_gatewayUrlTextBox.Focus();
return;
}
if (!GatewayUrlHelper.IsValidGatewayUrl(_gatewayUrlTextBox.Text.Trim()))
{
MessageBox.Show(GatewayUrlHelper.ValidationMessage, "Settings",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
_gatewayUrlTextBox.Focus();
return;
}
SaveSettings();
DialogResult = DialogResult.OK;
Close();
}
private void OnCancelClick(object? sender, EventArgs e)
{
DialogResult = DialogResult.Cancel;
Close();
}
}

View File

@ -1,153 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
namespace OpenClawTray;
public class SettingsManager
{
private static readonly string SettingsDirectory = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"OpenClawTray");
private static readonly string SettingsFile = Path.Combine(SettingsDirectory, "settings.json");
public string GatewayUrl { get; set; } = "ws://localhost:18789";
public string Token { get; set; } = "";
public bool AutoStart { get; set; } = false;
public bool ShowNotifications { get; set; } = true;
public string NotificationSound { get; set; } = "Default";
// Notification filters — which types to show toasts for
public bool NotifyHealth { get; set; } = true;
public bool NotifyUrgent { get; set; } = true;
public bool NotifyReminder { get; set; } = true;
public bool NotifyEmail { get; set; } = true;
public bool NotifyCalendar { get; set; } = true;
public bool NotifyBuild { get; set; } = true;
public bool NotifyStock { get; set; } = true;
public bool NotifyInfo { get; set; } = true;
// UI preferences
public bool ShowGlobalHotkey { get; set; } = true;
public bool MinimizeToTray { get; set; } = true;
public SettingsManager()
{
Load();
}
/// <summary>Check if a notification type should produce a toast.</summary>
public bool ShouldNotify(string type)
{
if (!ShowNotifications) return false;
return type switch
{
"health" => NotifyHealth,
"urgent" => NotifyUrgent,
"reminder" => NotifyReminder,
"email" => NotifyEmail,
"calendar" => NotifyCalendar,
"build" => NotifyBuild,
"stock" => NotifyStock,
"error" => NotifyUrgent, // Errors use urgent setting
"info" => NotifyInfo,
_ => NotifyInfo
};
}
public void Load()
{
try
{
if (File.Exists(SettingsFile))
{
var json = File.ReadAllText(SettingsFile);
var settings = JsonSerializer.Deserialize<SettingsData>(json);
if (settings != null)
{
GatewayUrl = settings.GatewayUrl ?? "ws://localhost:18789";
Token = settings.Token ?? "";
AutoStart = settings.AutoStart;
ShowNotifications = settings.ShowNotifications;
NotificationSound = settings.NotificationSound ?? "Default";
NotifyHealth = settings.NotifyHealth ?? true;
NotifyUrgent = settings.NotifyUrgent ?? true;
NotifyReminder = settings.NotifyReminder ?? true;
NotifyEmail = settings.NotifyEmail ?? true;
NotifyCalendar = settings.NotifyCalendar ?? true;
NotifyBuild = settings.NotifyBuild ?? true;
NotifyStock = settings.NotifyStock ?? true;
NotifyInfo = settings.NotifyInfo ?? true;
ShowGlobalHotkey = settings.ShowGlobalHotkey ?? true;
MinimizeToTray = settings.MinimizeToTray ?? true;
}
}
}
catch (Exception)
{
// Use defaults if loading fails
}
}
public void Save()
{
try
{
Directory.CreateDirectory(SettingsDirectory);
var settings = new SettingsData
{
GatewayUrl = GatewayUrl,
Token = Token,
AutoStart = AutoStart,
ShowNotifications = ShowNotifications,
NotificationSound = NotificationSound,
NotifyHealth = NotifyHealth,
NotifyUrgent = NotifyUrgent,
NotifyReminder = NotifyReminder,
NotifyEmail = NotifyEmail,
NotifyCalendar = NotifyCalendar,
NotifyBuild = NotifyBuild,
NotifyStock = NotifyStock,
NotifyInfo = NotifyInfo,
ShowGlobalHotkey = ShowGlobalHotkey,
MinimizeToTray = MinimizeToTray
};
var json = JsonSerializer.Serialize(settings, new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText(SettingsFile, json);
Logger.Info("Settings saved");
}
catch (Exception ex)
{
Logger.Error("Failed to save settings", ex);
throw new Exception($"Failed to save settings: {ex.Message}", ex);
}
}
public static string GetSettingsDirectory() => SettingsDirectory;
public static string GetSettingsFile() => SettingsFile;
private class SettingsData
{
public string? GatewayUrl { get; set; }
public string? Token { get; set; }
public bool AutoStart { get; set; }
public bool ShowNotifications { get; set; }
public string? NotificationSound { get; set; }
public bool? NotifyHealth { get; set; }
public bool? NotifyUrgent { get; set; }
public bool? NotifyReminder { get; set; }
public bool? NotifyEmail { get; set; }
public bool? NotifyCalendar { get; set; }
public bool? NotifyBuild { get; set; }
public bool? NotifyStock { get; set; }
public bool? NotifyInfo { get; set; }
public bool? ShowGlobalHotkey { get; set; }
public bool? MinimizeToTray { get; set; }
}
}

View File

@ -1,189 +0,0 @@
using OpenClaw.Shared;
using System;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
namespace OpenClawTray;
/// <summary>
/// Shows detailed gateway status, sessions, channels, and usage in a rich view.
/// </summary>
public class StatusDetailForm : ModernForm
{
private RichTextBox _textBox = null!;
private Button _refreshButton = null!;
private Button _closeButton = null!;
private readonly OpenClawGatewayClient? _client;
private readonly SettingsManager? _settings;
private readonly ConnectionStatus _status;
private static StatusDetailForm? _instance;
public static void ShowOrFocus(OpenClawGatewayClient? client, SettingsManager? settings, ConnectionStatus status)
{
if (_instance != null && !_instance.IsDisposed)
{
_instance.BringToFront();
_instance.Focus();
return;
}
_instance = new StatusDetailForm(client, settings, status);
_instance.Show();
}
private StatusDetailForm(OpenClawGatewayClient? client, SettingsManager? settings, ConnectionStatus status)
{
_client = client;
_settings = settings;
_status = status;
InitializeComponent();
RefreshStatus();
}
private void InitializeComponent()
{
Text = "OpenClaw Status";
Size = new Size(540, 520);
MinimumSize = new Size(420, 380);
FormBorderStyle = FormBorderStyle.Sizable;
Icon = IconHelper.GetLobsterIcon();
_textBox = new RichTextBox
{
Dock = DockStyle.Fill,
ReadOnly = true,
Font = new Font("Cascadia Code", 10F),
BackColor = IsDarkMode ? Color.FromArgb(25, 25, 25) : Color.FromArgb(252, 252, 252),
ForeColor = ForegroundColor,
BorderStyle = BorderStyle.None,
WordWrap = true,
Padding = new Padding(8)
};
var buttonPanel = new Panel
{
Dock = DockStyle.Bottom,
Height = 56,
BackColor = SurfaceColor,
Padding = new Padding(16, 12, 16, 12)
};
_closeButton = CreateModernButton("Close");
_closeButton.Size = new Size(90, 36);
_closeButton.Anchor = AnchorStyles.Right | AnchorStyles.Top;
_closeButton.Click += (_, _) => Close();
_refreshButton = CreateModernButton("Refresh", isPrimary: true);
_refreshButton.Size = new Size(90, 36);
_refreshButton.Anchor = AnchorStyles.Right | AnchorStyles.Top;
_refreshButton.Click += async (_, _) =>
{
if (_client != null)
{
await _client.CheckHealthAsync();
await _client.RequestSessionsAsync();
await _client.RequestUsageAsync();
}
RefreshStatus();
};
// Use FlowLayoutPanel for proper button layout
var buttonFlow = new FlowLayoutPanel
{
Dock = DockStyle.Right,
FlowDirection = FlowDirection.RightToLeft,
AutoSize = true,
BackColor = Color.Transparent
};
buttonFlow.Controls.Add(_closeButton);
buttonFlow.Controls.Add(_refreshButton);
buttonPanel.Controls.Add(buttonFlow);
Controls.Add(_textBox);
Controls.Add(buttonPanel);
}
private void RefreshStatus()
{
var sb = new StringBuilder();
// Header
sb.AppendLine("🦞 OPENCLAW STATUS");
sb.AppendLine(new string('─', 40));
sb.AppendLine();
// Connection
var statusIcon = _status switch
{
ConnectionStatus.Connected => "🟢",
ConnectionStatus.Connecting => "🟡",
ConnectionStatus.Error => "🔴",
_ => "⚪"
};
sb.AppendLine($" Gateway: {statusIcon} {_status}");
sb.AppendLine($" URL: {_settings?.GatewayUrl ?? "not configured"}");
sb.AppendLine($" Token: {(_settings?.Token?.Length > 0 ? "" : "not set")}");
sb.AppendLine();
// Sessions
if (_client != null)
{
var sessions = _client.GetSessionList();
if (sessions.Length > 0)
{
sb.AppendLine("🧠 SESSIONS");
sb.AppendLine(new string('─', 40));
foreach (var s in sessions)
{
sb.AppendLine($" {s.DisplayText}");
if (s.Model != null)
sb.AppendLine($" Model: {s.Model}");
if (s.StartedAt != null)
sb.AppendLine($" Started: {s.StartedAt:HH:mm:ss}");
}
sb.AppendLine();
}
}
// App info
sb.AppendLine(" APP INFO");
sb.AppendLine(new string('─', 40));
sb.AppendLine($" Version: 1.0.0");
sb.AppendLine($" Runtime: {Environment.Version}");
sb.AppendLine($" OS: {Environment.OSVersion}");
sb.AppendLine($" Machine: {Environment.MachineName}");
sb.AppendLine($" PID: {Environment.ProcessId}");
sb.AppendLine($" Uptime: {GetUptime()}");
sb.AppendLine();
// Settings
sb.AppendLine("⚙️ SETTINGS");
sb.AppendLine(new string('─', 40));
sb.AppendLine($" Auto-start: {(_settings?.AutoStart == true ? "" : "")}");
sb.AppendLine($" Notifications: {(_settings?.ShowNotifications == true ? "" : "")}");
sb.AppendLine($" Sound: {_settings?.NotificationSound ?? "Default"}");
_textBox.Text = sb.ToString();
}
private static string GetUptime()
{
var elapsed = DateTime.Now - System.Diagnostics.Process.GetCurrentProcess().StartTime;
if (elapsed.TotalHours >= 1)
return $"{elapsed.Hours}h {elapsed.Minutes}m";
if (elapsed.TotalMinutes >= 1)
return $"{elapsed.Minutes}m {elapsed.Seconds}s";
return $"{elapsed.Seconds}s";
}
protected override void OnFormClosed(FormClosedEventArgs e)
{
_instance = null;
base.OnFormClosed(e);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,85 +0,0 @@
using System;
using System.Drawing;
using System.Windows.Forms;
namespace OpenClawTray;
public enum UpdateDialogResult
{
Download,
RemindLater,
Skip
}
public class UpdateDialog : ModernForm
{
public UpdateDialogResult Result { get; private set; } = UpdateDialogResult.RemindLater;
public UpdateDialog(string version, string releaseNotes)
{
Text = "Update Available — OpenClaw Tray";
Size = new Size(500, 420);
Icon = IconHelper.GetLobsterIcon();
var titleLabel = CreateModernLabel("🦞 Update Available!");
titleLabel.Font = new Font("Segoe UI", 14, FontStyle.Bold);
titleLabel.ForeColor = AccentColor;
titleLabel.Location = new Point(20, 20);
Controls.Add(titleLabel);
var versionLabel = CreateModernLabel($"Version {version} is ready to install");
versionLabel.Location = new Point(20, 55);
Controls.Add(versionLabel);
var notesLabel = CreateModernLabel("Release Notes:");
notesLabel.Font = new Font("Segoe UI", 9.5f, FontStyle.Bold);
notesLabel.Location = new Point(20, 90);
Controls.Add(notesLabel);
var notesBox = CreateModernTextBox();
notesBox.Text = string.IsNullOrWhiteSpace(releaseNotes) ? "No release notes available." : releaseNotes;
notesBox.Multiline = true;
notesBox.ReadOnly = true;
notesBox.ScrollBars = ScrollBars.Vertical;
notesBox.Location = new Point(20, 115);
notesBox.Size = new Size(444, 200);
Controls.Add(notesBox);
var skipButton = CreateModernButton("Skip Version");
skipButton.Size = new Size(120, 36);
skipButton.Location = new Point(20, 330);
skipButton.Click += (_, _) =>
{
Result = UpdateDialogResult.Skip;
DialogResult = DialogResult.Cancel;
Close();
};
Controls.Add(skipButton);
var remindButton = CreateModernButton("Remind Later");
remindButton.Size = new Size(120, 36);
remindButton.Location = new Point(230, 330);
remindButton.Click += (_, _) =>
{
Result = UpdateDialogResult.RemindLater;
DialogResult = DialogResult.Cancel;
Close();
};
Controls.Add(remindButton);
var downloadButton = CreateModernButton("Download && Install", isPrimary: true);
downloadButton.Size = new Size(140, 36);
downloadButton.Location = new Point(324, 330);
downloadButton.Click += (_, _) =>
{
Result = UpdateDialogResult.Download;
DialogResult = DialogResult.OK;
Close();
};
Controls.Add(downloadButton);
AcceptButton = downloadButton;
CancelButton = remindButton;
}
}

View File

@ -1,187 +0,0 @@
using Microsoft.Web.WebView2.WinForms;
using Microsoft.Web.WebView2.Core;
using System;
using System.Drawing;
using System.IO;
using System.Threading.Tasks;
using System.Windows.Forms;
using Microsoft.Win32;
namespace OpenClawTray;
/// <summary>
/// Embeds the OpenClaw WebChat UI via WebView2 with modern Windows 11 styling.
/// </summary>
public class WebChatForm : ModernForm
{
private WebView2? _webView;
private readonly string _gatewayUrl;
private readonly string _token;
private Panel? _toolbar;
private bool _initialized;
private static WebChatForm? _instance;
public static void ShowOrFocus(string gatewayUrl, string token)
{
if (_instance != null && !_instance.IsDisposed)
{
_instance.BringToFront();
_instance.Focus();
return;
}
_instance = new WebChatForm(gatewayUrl, token);
_instance.Show();
}
private WebChatForm(string gatewayUrl, string token)
{
_gatewayUrl = gatewayUrl;
_token = token;
InitializeComponent();
_ = InitializeWebViewAsync();
}
private void InitializeComponent()
{
Text = "OpenClaw Chat";
Size = new Size(520, 750);
MinimumSize = new Size(380, 450);
FormBorderStyle = FormBorderStyle.Sizable;
Icon = IconHelper.GetLobsterIcon();
// Modern toolbar panel - generous height for emoji rendering
_toolbar = new Panel
{
Dock = DockStyle.Top,
Height = 50,
BackColor = SurfaceColor
};
var btnY = 8;
var homeBtn = CreateToolbarButton("🏠", "Home");
homeBtn.Location = new Point(8, btnY);
homeBtn.Click += (_, _) => NavigateToChat();
var refreshBtn = CreateToolbarButton("↻", "Refresh");
refreshBtn.Location = new Point(50, btnY);
refreshBtn.Click += (_, _) => _webView?.Reload();
var popoutBtn = CreateToolbarButton("↗", "Open in Browser");
popoutBtn.Location = new Point(92, btnY);
popoutBtn.Click += (_, _) =>
{
var url = _gatewayUrl.Replace("ws://", "http://").Replace("wss://", "https://");
try { System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo($"{url}?token={Uri.EscapeDataString(_token)}") { UseShellExecute = true }); }
catch { }
};
var devToolsBtn = CreateToolbarButton("🔧", "DevTools");
devToolsBtn.Location = new Point(134, btnY);
devToolsBtn.Click += (_, _) => _webView?.CoreWebView2?.OpenDevToolsWindow();
_toolbar.Controls.AddRange(new Control[] { homeBtn, refreshBtn, popoutBtn, devToolsBtn });
// WebView2 fills remaining space
_webView = new WebView2
{
Dock = DockStyle.Fill,
DefaultBackgroundColor = IsDarkMode ? Color.FromArgb(25, 25, 25) : Color.FromArgb(250, 250, 250)
};
Controls.Add(_webView);
Controls.Add(_toolbar);
}
private Button CreateToolbarButton(string icon, string tooltip)
{
var btn = new Button
{
Text = icon,
Size = new Size(38, 34),
FlatStyle = FlatStyle.Flat,
Font = new Font("Segoe UI Symbol", 12),
Cursor = Cursors.Hand,
BackColor = Color.Transparent,
ForeColor = ForegroundColor,
UseCompatibleTextRendering = true
};
btn.FlatAppearance.BorderSize = 0;
btn.FlatAppearance.MouseOverBackColor = HoverColor;
var toolTip = new ToolTip();
toolTip.SetToolTip(btn, tooltip);
return btn;
}
private async Task InitializeWebViewAsync()
{
try
{
var userDataDir = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"OpenClawTray", "WebView2");
var env = await CoreWebView2Environment.CreateAsync(userDataFolder: userDataDir);
await _webView!.EnsureCoreWebView2Async(env);
var settings = _webView.CoreWebView2.Settings;
settings.IsStatusBarEnabled = false;
settings.AreDefaultContextMenusEnabled = true;
settings.IsZoomControlEnabled = true;
_initialized = true;
Logger.Info("WebView2 initialized");
NavigateToChat();
}
catch (WebView2RuntimeNotFoundException)
{
Logger.Error("WebView2 runtime not found");
var result = MessageBox.Show(
"The Microsoft WebView2 Runtime is required for the chat panel.\n\n" +
"Would you like to download it?",
"WebView2 Required",
MessageBoxButtons.YesNo,
MessageBoxIcon.Warning);
if (result == DialogResult.Yes)
{
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(
"https://developer.microsoft.com/en-us/microsoft-edge/webview2/")
{ UseShellExecute = true });
}
Close();
}
catch (Exception ex)
{
Logger.Error("WebView2 init failed", ex);
MessageBox.Show($"Failed to initialize chat panel:\n{ex.Message}",
"Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
Close();
}
}
private void NavigateToChat()
{
if (!_initialized || _webView?.CoreWebView2 == null) return;
var httpUrl = _gatewayUrl
.Replace("ws://", "http://")
.Replace("wss://", "https://");
var chatUrl = $"{httpUrl}?token={Uri.EscapeDataString(_token)}";
_webView.CoreWebView2.Navigate(chatUrl);
Logger.Info($"Navigating to WebChat: {httpUrl}");
}
protected override void OnFormClosed(FormClosedEventArgs e)
{
_webView?.Dispose();
_instance = null;
base.OnFormClosed(e);
}
}

View File

@ -1,95 +0,0 @@
using System;
using System.Diagnostics;
using System.Drawing;
using System.Windows.Forms;
namespace OpenClawTray;
/// <summary>
/// First-run welcome dialog to help users get started with OpenClaw.
/// </summary>
public class WelcomeDialog : ModernForm
{
private readonly string _dashboardUrl;
public WelcomeDialog(string dashboardUrl)
{
_dashboardUrl = dashboardUrl;
InitializeComponent();
}
private void InitializeComponent()
{
Text = "Welcome to Molty";
Size = new Size(500, 380);
FormBorderStyle = FormBorderStyle.FixedDialog;
MaximizeBox = false;
MinimizeBox = false;
StartPosition = FormStartPosition.CenterScreen;
Icon = IconHelper.GetLobsterIcon();
var y = 20;
// Lobster header
var headerLabel = new Label
{
Text = "🦞",
Font = new Font("Segoe UI Emoji", 36),
Location = new Point(0, y),
Size = new Size(ClientSize.Width, 60),
TextAlign = ContentAlignment.MiddleCenter,
ForeColor = AccentColor
};
y += 70;
// Welcome text
var welcomeLabel = new Label
{
Text = "Welcome to Molty!",
Font = new Font("Segoe UI", 14, FontStyle.Bold),
Location = new Point(0, y),
Size = new Size(ClientSize.Width, 30),
TextAlign = ContentAlignment.MiddleCenter,
ForeColor = ForegroundColor,
BackColor = Color.Transparent
};
y += 40;
// Instructions
var instructionsLabel = CreateModernLabel(
"To get started, you'll need an API token from your\n" +
"OpenClaw dashboard. Click below to learn how to get one,\n" +
"then paste your token in Settings.");
instructionsLabel.Font = new Font("Segoe UI", 9.5f);
instructionsLabel.Location = new Point(30, y);
instructionsLabel.Size = new Size(ClientSize.Width - 60, 60);
instructionsLabel.TextAlign = ContentAlignment.MiddleCenter;
y += 85;
// Learn about tokens button
var learnBtn = CreateModernButton("Learn How to Get a Token", isPrimary: true);
learnBtn.Location = new Point((ClientSize.Width - 250) / 2, y);
learnBtn.Size = new Size(250, 40);
learnBtn.Click += (_, _) =>
{
try
{
Process.Start(new ProcessStartInfo("https://docs.molt.bot/web/dashboard") { UseShellExecute = true });
}
catch { }
};
y += 55;
// Open Settings button
var settingsBtn = CreateModernButton("Open Settings");
settingsBtn.Location = new Point((ClientSize.Width - 160) / 2, y);
settingsBtn.Size = new Size(160, 36);
settingsBtn.Click += (_, _) =>
{
DialogResult = DialogResult.OK;
Close();
};
Controls.AddRange(new Control[] { headerLabel, welcomeLabel, instructionsLabel, learnBtn, settingsBtn });
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

View File

@ -1,26 +0,0 @@
# Screenshots
This directory contains screenshots for the README and documentation.
## Required Screenshots
1. **tray-menu.png** - System tray icon with context menu open
2. **settings-dialog.png** - Settings configuration dialog
3. **quick-send-dialog.png** - Quick send message dialog
4. **notification-example.png** - Example of Windows toast notification
## Taking Screenshots
1. Run the application
2. Right-click the tray icon to show the menu
3. Open various dialogs and take screenshots
4. Use tools like Snipping Tool or Windows+Shift+S
5. Save as PNG files with descriptive names
## Image Guidelines
- Use PNG format for transparency support
- Keep file sizes reasonable (< 500KB each)
- Show the application in a clean Windows environment
- Include relevant context (Windows version, other tray icons, etc.)
- Ensure text is readable at normal viewing sizes