diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index bbfe3c7..7b93343 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -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
diff --git a/moltbot-windows-hub.slnx b/moltbot-windows-hub.slnx
index bac046f..627f0f5 100644
--- a/moltbot-windows-hub.slnx
+++ b/moltbot-windows-hub.slnx
@@ -4,7 +4,6 @@
-
diff --git a/src/OpenClaw.Tray/.gitignore b/src/OpenClaw.Tray/.gitignore
deleted file mode 100644
index 3ae99a0..0000000
--- a/src/OpenClaw.Tray/.gitignore
+++ /dev/null
@@ -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
\ No newline at end of file
diff --git a/src/OpenClaw.Tray/AutoStartManager.cs b/src/OpenClaw.Tray/AutoStartManager.cs
deleted file mode 100644
index 1d030ef..0000000
--- a/src/OpenClaw.Tray/AutoStartManager.cs
+++ /dev/null
@@ -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;
- }
-}
diff --git a/src/OpenClaw.Tray/DEVELOPMENT.md b/src/OpenClaw.Tray/DEVELOPMENT.md
deleted file mode 100644
index 92dac58..0000000
--- a/src/OpenClaw.Tray/DEVELOPMENT.md
+++ /dev/null
@@ -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)
-
-
diff --git a/src/OpenClaw.Tray/DeepLinkHandler.cs b/src/OpenClaw.Tray/DeepLinkHandler.cs
deleted file mode 100644
index c5fd51e..0000000
--- a/src/OpenClaw.Tray/DeepLinkHandler.cs
+++ /dev/null
@@ -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;
-
-///
-/// Handles openclaw:// URI scheme registration and processing.
-/// Matches macOS deep link support (openclaw://agent?message=...)
-///
-public static class DeepLinkHandler
-{
- private const string UriScheme = "OpenClaw";
- private const string FriendlyName = "OpenClaw Agent Command";
-
- ///
- /// Registers the openclaw:// URI scheme in the Windows registry.
- /// Requires elevation for HKCR, falls back to HKCU.
- ///
- 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);
- }
- }
-
- ///
- /// Checks if the app was launched with a deep link argument.
- ///
- 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;
- }
-
- ///
- /// 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
- ///
- public static async Task ProcessDeepLinkAsync(Uri uri, OpenClawGatewayClient client, Action? openDashboard = null, Action? openChat = null, Action? openSettings = null, Action? 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 { }
- }
- }
-}
-
diff --git a/src/OpenClaw.Tray/DownloadProgressDialog.cs b/src/OpenClaw.Tray/DownloadProgressDialog.cs
deleted file mode 100644
index 8117e48..0000000
--- a/src/OpenClaw.Tray/DownloadProgressDialog.cs
+++ /dev/null
@@ -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);
- }
-}
-
diff --git a/src/OpenClaw.Tray/GlobalHotkey.cs b/src/OpenClaw.Tray/GlobalHotkey.cs
deleted file mode 100644
index abfe876..0000000
--- a/src/OpenClaw.Tray/GlobalHotkey.cs
+++ /dev/null
@@ -1,94 +0,0 @@
-using System;
-using System.Runtime.InteropServices;
-using System.Windows.Forms;
-
-namespace OpenClawTray;
-
-///
-/// Registers a system-wide hotkey that works even when the app is not focused.
-/// Default: Ctrl+Alt+Shift+C to open Quick Send.
-///
-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();
- }
- }
-}
-
diff --git a/src/OpenClaw.Tray/IconHelper.cs b/src/OpenClaw.Tray/IconHelper.cs
deleted file mode 100644
index 200ac8a..0000000
--- a/src/OpenClaw.Tray/IconHelper.cs
+++ /dev/null
@@ -1,95 +0,0 @@
-using System.Drawing;
-
-namespace OpenClawTray;
-
-///
-/// Shared icon helper for creating the lobster icon used throughout the app.
-///
-public static class IconHelper
-{
- private static Icon? _cachedLobsterIcon;
-
- ///
- /// Gets the lobster icon for use in forms and windows.
- ///
- 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);
- }
-}
-
diff --git a/src/OpenClaw.Tray/LICENSE b/src/OpenClaw.Tray/LICENSE
deleted file mode 100644
index 4c7463e..0000000
--- a/src/OpenClaw.Tray/LICENSE
+++ /dev/null
@@ -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.
\ No newline at end of file
diff --git a/src/OpenClaw.Tray/Logger.cs b/src/OpenClaw.Tray/Logger.cs
deleted file mode 100644
index f70b26a..0000000
--- a/src/OpenClaw.Tray/Logger.cs
+++ /dev/null
@@ -1,109 +0,0 @@
-using System;
-using System.Diagnostics;
-using System.IO;
-using OpenClaw.Shared;
-
-namespace OpenClawTray;
-
-///
-/// Simple file + debug logger for troubleshooting.
-/// Writes to %LOCALAPPDATA%\OpenClawTray\openclaw-tray.log
-///
-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;
-
- /// Get a logger instance that implements IOpenClawLogger for the shared library.
- 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}");
-
- /// Flush and close the log file (call on app exit).
- 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
- }
- }
-
- /// Adapter to make the static Logger work with IOpenClawLogger interface.
- 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);
- }
- }
-}
-
-
diff --git a/src/OpenClaw.Tray/ModernForm.cs b/src/OpenClaw.Tray/ModernForm.cs
deleted file mode 100644
index da0c21d..0000000
--- a/src/OpenClaw.Tray/ModernForm.cs
+++ /dev/null
@@ -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;
-
-///
-/// Base form with Windows 11 modern styling - dark/light mode, rounded corners, OpenClaw branding.
-/// Inherit from this for consistent look across all dialogs.
-///
-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));
- }
-
- ///
- /// Creates a styled button with OpenClaw branding.
- ///
- 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;
- }
-
- ///
- /// Creates a styled text box.
- ///
- protected TextBox CreateModernTextBox()
- {
- return new TextBox
- {
- Font = new Font("Segoe UI", 10f),
- BackColor = SurfaceColor,
- ForeColor = ForegroundColor,
- BorderStyle = BorderStyle.FixedSingle
- };
- }
-
- ///
- /// Creates a styled label.
- ///
- 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
- };
- }
-
- ///
- /// Creates a styled checkbox.
- ///
- 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;
- }
-
- ///
- /// Creates a styled group box.
- ///
- protected GroupBox CreateModernGroupBox(string text)
- {
- return new GroupBox
- {
- Text = text,
- Font = new Font("Segoe UI", 9.5f, FontStyle.Bold),
- ForeColor = AccentColor,
- BackColor = Color.Transparent
- };
- }
-
- ///
- /// Creates a styled panel with border.
- ///
- protected Panel CreateModernPanel()
- {
- return new Panel
- {
- BackColor = SurfaceColor,
- BorderStyle = BorderStyle.None,
- Padding = new Padding(12)
- };
- }
-
- ///
- /// Creates a horizontal separator line.
- ///
- protected Panel CreateSeparator()
- {
- return new Panel
- {
- Height = 1,
- BackColor = BorderColor,
- Dock = DockStyle.Top,
- Margin = new Padding(0, 8, 0, 8)
- };
- }
-
- ///
- /// Creates a styled progress bar.
- ///
- protected ProgressBar CreateModernProgressBar()
- {
- return new ProgressBar
- {
- Style = ProgressBarStyle.Continuous,
- Height = 6,
- ForeColor = AccentColor
- };
- }
-}
diff --git a/src/OpenClaw.Tray/ModernForm.resx b/src/OpenClaw.Tray/ModernForm.resx
deleted file mode 100644
index 1af7de1..0000000
--- a/src/OpenClaw.Tray/ModernForm.resx
+++ /dev/null
@@ -1,120 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- text/microsoft-resx
-
-
- 2.0
-
-
- System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
-
-
- System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
-
-
\ No newline at end of file
diff --git a/src/OpenClaw.Tray/ModernTrayMenu.cs b/src/OpenClaw.Tray/ModernTrayMenu.cs
deleted file mode 100644
index c8834cb..0000000
--- a/src/OpenClaw.Tray/ModernTrayMenu.cs
+++ /dev/null
@@ -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;
-
-///
-/// Modern flyout menu with Windows 11 styling - dark/light mode, rounded corners, acrylic blur.
-/// Replaces the dated ContextMenuStrip with a custom-drawn popup.
-///
-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 _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? 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; }
- }
-}
diff --git a/src/OpenClaw.Tray/NotificationHistoryForm.cs b/src/OpenClaw.Tray/NotificationHistoryForm.cs
deleted file mode 100644
index db6df11..0000000
--- a/src/OpenClaw.Tray/NotificationHistoryForm.cs
+++ /dev/null
@@ -1,166 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Drawing;
-using System.Windows.Forms;
-
-namespace OpenClawTray;
-
-///
-/// Shows recent notification history in a modern styled list view.
-///
-public class NotificationHistoryForm : ModernForm
-{
- private ListView? _listView;
- private Button _clearButton = null!;
- private Button _closeButton = null!;
- private static NotificationHistoryForm? _instance;
-
- private static readonly List _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; } = "";
- }
-}
-
-
diff --git a/src/OpenClaw.Tray/OpenClaw.Tray.csproj b/src/OpenClaw.Tray/OpenClaw.Tray.csproj
deleted file mode 100644
index 0fbeaad..0000000
--- a/src/OpenClaw.Tray/OpenClaw.Tray.csproj
+++ /dev/null
@@ -1,39 +0,0 @@
-
-
-
- WinExe
- net10.0-windows10.0.19041.0
- true
- enable
- enable
- true
- MSB3277
- openclaw.ico
- OpenClaw Windows Tray
- OpenClaw
- Scott Hanselman
- OpenClaw Tray
- Copyright © 2026 Scott Hanselman
- 0.4.4
- true
- true
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/OpenClaw.Tray/Program.cs b/src/OpenClaw.Tray/Program.cs
deleted file mode 100644
index 6e5364a..0000000
--- a/src/OpenClaw.Tray/Program.cs
+++ /dev/null
@@ -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 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 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 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);
- }
- }
- });
- }
-}
-
diff --git a/src/OpenClaw.Tray/QuickSendDialog.cs b/src/OpenClaw.Tray/QuickSendDialog.cs
deleted file mode 100644
index 19100a0..0000000
--- a/src/OpenClaw.Tray/QuickSendDialog.cs
+++ /dev/null
@@ -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);
- }
-}
-
diff --git a/src/OpenClaw.Tray/README.md b/src/OpenClaw.Tray/README.md
deleted file mode 100644
index ab8d1b7..0000000
--- a/src/OpenClaw.Tray/README.md
+++ /dev/null
@@ -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://: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
-
-
-
diff --git a/src/OpenClaw.Tray/SettingsDialog.cs b/src/OpenClaw.Tray/SettingsDialog.cs
deleted file mode 100644
index f1807d9..0000000
--- a/src/OpenClaw.Tray/SettingsDialog.cs
+++ /dev/null
@@ -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();
- }
-}
-
diff --git a/src/OpenClaw.Tray/SettingsManager.cs b/src/OpenClaw.Tray/SettingsManager.cs
deleted file mode 100644
index 8975ec7..0000000
--- a/src/OpenClaw.Tray/SettingsManager.cs
+++ /dev/null
@@ -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();
- }
-
- /// Check if a notification type should produce a toast.
- 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(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; }
- }
-}
diff --git a/src/OpenClaw.Tray/StatusDetailForm.cs b/src/OpenClaw.Tray/StatusDetailForm.cs
deleted file mode 100644
index f724643..0000000
--- a/src/OpenClaw.Tray/StatusDetailForm.cs
+++ /dev/null
@@ -1,189 +0,0 @@
-using OpenClaw.Shared;
-using System;
-using System.Drawing;
-using System.Text;
-using System.Windows.Forms;
-
-namespace OpenClawTray;
-
-///
-/// Shows detailed gateway status, sessions, channels, and usage in a rich view.
-///
-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);
- }
-}
-
-
diff --git a/src/OpenClaw.Tray/TrayApplication.cs b/src/OpenClaw.Tray/TrayApplication.cs
deleted file mode 100644
index f46a30b..0000000
--- a/src/OpenClaw.Tray/TrayApplication.cs
+++ /dev/null
@@ -1,1136 +0,0 @@
-using Microsoft.Toolkit.Uwp.Notifications;
-using OpenClaw.Shared;
-using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.Drawing;
-using System.Linq;
-using System.Runtime.InteropServices;
-using System.Threading;
-using System.Threading.Tasks;
-using System.Windows.Forms;
-using ActivityKind = OpenClaw.Shared.ActivityKind;
-
-namespace OpenClawTray;
-
-public class TrayApplication : ApplicationContext
-{
- private NotifyIcon? _notifyIcon;
- private ContextMenuStrip? _contextMenu;
- private ModernTrayMenu? _modernMenu;
- private OpenClawGatewayClient? _gatewayClient;
- private SettingsManager? _settings;
- private System.Windows.Forms.Timer? _healthCheckTimer;
- private System.Windows.Forms.Timer? _sessionPollTimer;
- private GlobalHotkey? _globalHotkey;
- private ConnectionStatus _currentStatus = ConnectionStatus.Disconnected;
- private AgentActivity? _currentActivity;
- private readonly SynchronizationContext? _syncContext;
-
- // Session-aware activity: track per-session state to avoid flip-flopping
- private readonly Dictionary _sessionActivities = new();
- private string? _displayedSessionKey;
- private DateTime _lastSessionSwitch = DateTime.MinValue;
- private DateTime _lastCheckTime = DateTime.Now;
- private static readonly TimeSpan SessionSwitchDebounce = TimeSpan.FromSeconds(3);
-
- // Menu items for dynamic updates
- private ToolStripMenuItem? _statusItem;
- private ToolStripMenuItem? _activityItem;
- private ToolStripMenuItem? _usageItem;
- private ToolStripSeparator? _channelSeparator;
- private ToolStripSeparator? _sessionSeparator;
- private readonly List _channelItems = new();
- private readonly List _sessionItems = new();
-
- // Channel and session data for modern menu
- private ChannelHealth[] _lastChannels = Array.Empty();
- private SessionInfo[] _lastSessions = Array.Empty();
- private GatewayUsageInfo? _lastUsage;
-
- private readonly string[] _startupArgs;
-
- // P/Invoke for proper icon cleanup
- [DllImport("user32.dll", CharSet = CharSet.Auto)]
- private static extern bool DestroyIcon(IntPtr handle);
-
- public TrayApplication(string[]? args = null)
- {
- _startupArgs = args ?? Array.Empty();
- _syncContext = SynchronizationContext.Current ?? new WindowsFormsSynchronizationContext();
- Logger.Info("Application starting");
- try
- {
- InitializeComponent();
- InitializeAsync();
- }
- catch (Exception ex)
- {
- Logger.Error($"Failed to initialize: {ex}");
- throw;
- }
- }
-
- private void InitializeComponent()
- {
- _settings = new SettingsManager();
-
- // First-run check: show welcome if no token configured
- if (string.IsNullOrWhiteSpace(_settings.Token))
- {
- ShowFirstRunWelcome();
- }
-
- // Register toast activation handler
- ToastNotificationManagerCompat.OnActivated += OnToastActivated;
-
- _contextMenu = new ContextMenuStrip();
-
- // Title
- var titleItem = new ToolStripMenuItem("⚡ OpenClaw Tray") { Enabled = false };
- _contextMenu.Items.Add(titleItem);
- _contextMenu.Items.Add(new ToolStripSeparator());
-
- // Status (clickable — opens detail view)
- _statusItem = new ToolStripMenuItem("Status: Disconnected");
- _statusItem.Click += OnShowStatusDetail;
- _contextMenu.Items.Add(_statusItem);
-
- // Activity (hidden when idle)
- _activityItem = new ToolStripMenuItem("") { Enabled = false, Visible = false };
- _contextMenu.Items.Add(_activityItem);
-
- // Usage (hidden until data available)
- _usageItem = new ToolStripMenuItem("") { Enabled = false, Visible = false };
- _contextMenu.Items.Add(_usageItem);
-
- // Session separator + placeholder
- _sessionSeparator = new ToolStripSeparator { Visible = false };
- _contextMenu.Items.Add(_sessionSeparator);
-
- // Channel health separator + placeholder
- _channelSeparator = new ToolStripSeparator { Visible = false };
- _contextMenu.Items.Add(_channelSeparator);
-
- _contextMenu.Items.Add(new ToolStripSeparator());
-
- // Actions
- _contextMenu.Items.Add("Open Dashboard", null, OnOpenDashboard);
- _contextMenu.Items.Add("Open Web Chat", null, OnOpenWebUI);
- _contextMenu.Items.Add("Quick Send...", null, OnQuickSend);
- _contextMenu.Items.Add("Notification History...", null, OnNotificationHistory);
- _contextMenu.Items.Add("Run Health Check", null, OnManualHealthCheck);
- _contextMenu.Items.Add(new ToolStripSeparator());
-
- // Settings
- _contextMenu.Items.Add("Settings...", null, OnSettings);
- var autoStartMenuItem = new ToolStripMenuItem("Auto-start", null, OnToggleAutoStart)
- {
- Checked = _settings.AutoStart
- };
- _contextMenu.Items.Add(autoStartMenuItem);
- _contextMenu.Items.Add(new ToolStripSeparator());
-
- // Log file access
- _contextMenu.Items.Add("Open Log File", null, OnOpenLogFile);
- _contextMenu.Items.Add("Exit", null, OnExit);
-
- // Modern tray menu (Windows 11 style)
- _modernMenu = new ModernTrayMenu();
- _modernMenu.MenuItemClicked += OnModernMenuItemClicked;
-
- // Tray icon - use modern menu on right-click
- _notifyIcon = new NotifyIcon
- {
- Icon = CreateStatusIcon(ConnectionStatus.Disconnected),
- Text = "OpenClaw Tray — Disconnected",
- Visible = true
- };
- _notifyIcon.MouseClick += OnTrayIconClick;
- _notifyIcon.DoubleClick += OnDoubleClick;
-
- // Health check timer (30s)
- _healthCheckTimer = new System.Windows.Forms.Timer { Interval = 30000, Enabled = true };
- _healthCheckTimer.Tick += OnHealthCheck;
-
- // Session/usage poll timer (60s) — less frequent
- _sessionPollTimer = new System.Windows.Forms.Timer { Interval = 60000, Enabled = true };
- _sessionPollTimer.Tick += OnSessionPoll;
-
- // Global hotkey: Ctrl+Alt+Shift+C → Quick Send
- _globalHotkey = new GlobalHotkey();
- _globalHotkey.HotkeyPressed += (_, _) => OnQuickSend(null, EventArgs.Empty);
- _globalHotkey.Register();
- }
-
- private async void OnTrayIconClick(object? sender, MouseEventArgs e)
- {
- if (e.Button == MouseButtons.Right || e.Button == MouseButtons.Left)
- {
- // Request fresh data before showing menu
- if (_gatewayClient != null && _currentStatus == ConnectionStatus.Connected)
- {
- try
- {
- // Fire off requests - don't await, just let them update the cache
- _ = _gatewayClient.CheckHealthAsync();
- _ = _gatewayClient.RequestSessionsAsync();
- _ = _gatewayClient.RequestUsageAsync();
- // Small delay to let responses arrive
- await Task.Delay(150);
- }
- catch { /* ignore - show cached data */ }
- }
-
- // Build and show modern menu
- BuildModernMenu();
- _modernMenu?.ShowAtCursor();
- }
- }
-
- private void BuildModernMenu()
- {
- if (_modernMenu == null) return;
-
- _modernMenu.ClearItems();
- Logger.Info("Building modern menu...");
-
- // Brand Header - big lobster!
- _modernMenu.AddBrandHeader("🦞", "Molty", "Made with 🦞 love by Scott Hanselman and Molty");
-
- // Status - use simple bullets that we can color
- var (statusIcon, statusText, statusColor) = _currentStatus switch
- {
- ConnectionStatus.Connected => ("●", "Connected", Color.FromArgb(46, 204, 113)),
- ConnectionStatus.Connecting => ("●", "Connecting...", Color.FromArgb(241, 196, 15)),
- ConnectionStatus.Error => ("●", "Error", Color.FromArgb(231, 76, 60)),
- _ => ("○", "Disconnected", Color.Gray)
- };
- _modernMenu.AddStatusItem("status", statusIcon, "Gateway", statusText, statusColor);
-
- // Activity (if active)
- if (_currentActivity?.Kind != ActivityKind.Idle && !string.IsNullOrEmpty(_currentActivity?.DisplayText))
- {
- _modernMenu.AddItem("activity", "▶", _currentActivity.DisplayText, enabled: false);
- }
-
- // Usage (if available)
- if (_lastUsage != null)
- {
- _modernMenu.AddItem("usage", "◆", _lastUsage.DisplayText, enabled: false);
- }
-
- _modernMenu.AddSeparator();
-
- // Sessions (if any) - show meaningful info, clickable to go to /sessions
- if (_lastSessions.Length > 0)
- {
- _modernMenu.AddItem("sessions", "◈", "Sessions", isHeader: true); // Clickable header!
- foreach (var session in _lastSessions.Take(5))
- {
- // Extract session type from key like "agent:main:cron:uuid" or "agent:main:subagent:uuid"
- var parts = session.Key.Split(':');
- var sessionType = parts.Length >= 3 ? parts[2] : "session";
- var displayName = sessionType switch
- {
- "main" => "Main Agent",
- "cron" => "Scheduled Task",
- "subagent" => "Sub-Agent",
- _ => sessionType.Length > 0 ? char.ToUpper(sessionType[0]) + sessionType[1..] : "Session"
- };
-
- // Add model if available
- if (!string.IsNullOrEmpty(session.Model))
- displayName += $" ({session.Model})";
- else if (!string.IsNullOrEmpty(session.Channel))
- displayName += $" · {session.Channel}";
-
- var icon = session.IsMain ? "★" : "·";
- _modernMenu.AddItem($"session:{session.Key}", icon, displayName, enabled: false);
- }
- if (_lastSessions.Length > 5)
- _modernMenu.AddItem("", "", $"+{_lastSessions.Length - 5} more...", enabled: false);
- _modernMenu.AddSeparator();
- }
-
- // Channels (if any)
- if (_lastChannels.Length > 0)
- {
- _modernMenu.AddItem("", "◉", "Channels", isHeader: true);
- foreach (var ch in _lastChannels)
- {
- var rawStatus = ch.Status?.ToLowerInvariant() ?? "";
-
- // Normalize status display
- // READY = configured and verified (linked or probe ok), ready to receive messages
- // IDLE = configured but not verified (needs setup)
- // ON = actively running/processing
- var (statusLabel, color) = rawStatus switch
- {
- "ok" or "connected" or "running" or "active" => ("ON", Color.FromArgb(46, 204, 113)),
- "ready" => ("READY", Color.FromArgb(46, 204, 113)),
- "stopped" or "idle" or "paused" => ("IDLE", Color.FromArgb(241, 196, 15)),
- "configured" or "pending" => ("IDLE", Color.FromArgb(241, 196, 15)),
- "error" or "disconnected" or "failed" => ("ERROR", Color.FromArgb(231, 76, 60)),
- "not configured" or "unconfigured" => ("N/A", Color.Gray),
- _ => ("OFF", Color.Gray)
- };
- _modernMenu.AddStatusItem($"channel:{ch.Name}", "○", char.ToUpper(ch.Name[0]) + ch.Name[1..], statusLabel, color);
- }
- _modernMenu.AddSeparator();
- }
-
- // Actions - use simple shapes we can color
- _modernMenu.AddItem("dashboard", "◐", "Open Dashboard");
- _modernMenu.AddItem("webchat", "◉", "Open Web Chat");
- _modernMenu.AddItem("quicksend", "▷", "Quick Send...");
- _modernMenu.AddItem("cron", "⏱", "Cron Jobs");
- _modernMenu.AddItem("history", "≡", "Notification History");
- _modernMenu.AddItem("servicehealth", "♥", "Service Health...");
-
- _modernMenu.AddSeparator();
-
- // Settings
- _modernMenu.AddItem("settings", "⚙", "Settings...");
- _modernMenu.AddItem("autostart", _settings?.AutoStart == true ? "✓" : "○",
- _settings?.AutoStart == true ? "Auto-start: On" : "Auto-start: Off");
- _modernMenu.AddItem("logs", "▤", "Open Log File");
-
- _modernMenu.AddSeparator();
- _modernMenu.AddItem("exit", "✕", "Exit");
- }
-
- private void OnModernMenuItemClicked(object? sender, string id)
- {
- switch (id)
- {
- case "status":
- OnShowStatusDetail(null, EventArgs.Empty);
- break;
- case "dashboard":
- OnOpenDashboard(null, EventArgs.Empty);
- break;
- case "webchat":
- OnOpenWebUI(null, EventArgs.Empty);
- break;
- case "quicksend":
- OnQuickSend(null, EventArgs.Empty);
- break;
- case "history":
- OnNotificationHistory(null, EventArgs.Empty);
- break;
- case "servicehealth":
- OnShowStatusDetail(null, EventArgs.Empty);
- break;
- case "sessions":
- OpenDashboardPath("/sessions");
- break;
- case "cron":
- OpenDashboardPath("/cron");
- break;
- case "settings":
- OnSettings(null, EventArgs.Empty);
- break;
- case "autostart":
- OnToggleAutoStart(null, EventArgs.Empty);
- break;
- case "logs":
- OnOpenLogFile(null, EventArgs.Empty);
- break;
- case "exit":
- OnExit(null, EventArgs.Empty);
- break;
- default:
- // Handle channel toggle: "channel:telegram" etc.
- if (id.StartsWith("channel:"))
- {
- var channelName = id[8..]; // Remove "channel:" prefix
- _ = ToggleChannelAsync(channelName);
- }
- break;
- }
- }
-
- private void OpenDashboardPath(string path)
- {
- var dashboardUrl = GetDashboardUrl().TrimEnd('/') + path;
- try
- {
- System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
- {
- FileName = dashboardUrl,
- UseShellExecute = true
- });
- }
- catch (Exception ex)
- {
- Logger.Instance.Error($"Failed to open dashboard path {path}", ex);
- }
- }
-
- private async Task ToggleChannelAsync(string channelName)
- {
- if (_gatewayClient == null) return;
-
- // Find the channel to check its current status
- var channel = _lastChannels.FirstOrDefault(c => c.Name.Equals(channelName, StringComparison.OrdinalIgnoreCase));
- if (channel == null) return;
-
- var isRunning = ChannelHealth.IsHealthyStatus(channel.Status);
-
- if (isRunning)
- {
- Logger.Info($"Stopping channel: {channelName}");
- await _gatewayClient.StopChannelAsync(channelName);
- }
- else
- {
- Logger.Info($"Starting channel: {channelName}");
- await _gatewayClient.StartChannelAsync(channelName);
- }
-
- // Request fresh health data after a short delay
- await Task.Delay(500);
- await _gatewayClient.CheckHealthAsync();
- }
-
- private async void InitializeAsync()
- {
- try
- {
- _gatewayClient = new OpenClawGatewayClient(_settings!.GatewayUrl, _settings.Token, Logger.Instance);
- _gatewayClient.StatusChanged += OnStatusChanged;
- _gatewayClient.NotificationReceived += OnNotificationReceived;
- _gatewayClient.ActivityChanged += OnActivityChanged;
- _gatewayClient.ChannelHealthUpdated += OnChannelHealthUpdated;
- _gatewayClient.SessionsUpdated += OnSessionsUpdated;
- _gatewayClient.UsageUpdated += OnUsageUpdated;
-
- await _gatewayClient.ConnectAsync();
-
- // Process deep link if launched via URI
- if (DeepLinkHandler.TryGetDeepLink(_startupArgs, out var uri) && uri != null)
- {
- await ProcessDeepLinkInternalAsync(uri);
- }
-
- // Start IPC server for deep links from other instances
- Program.StartDeepLinkServer(OnDeepLinkReceivedViaIpc);
- }
- catch (Exception ex)
- {
- Logger.Error("Initial connection failed", ex);
- ShowErrorToast("Connection Failed", $"Failed to connect: {ex.Message}");
- }
- }
-
- private void OnDeepLinkReceivedViaIpc(string uriString)
- {
- if (Uri.TryCreate(uriString, UriKind.Absolute, out var uri))
- {
- _syncContext?.Post(async _ =>
- {
- await ProcessDeepLinkInternalAsync(uri);
- }, null);
- }
- }
-
- private async Task ProcessDeepLinkInternalAsync(Uri uri)
- {
- if (_gatewayClient == null) return;
-
- await DeepLinkHandler.ProcessDeepLinkAsync(
- uri,
- _gatewayClient,
- openDashboard: path => OpenDashboardPath("/" + path.TrimStart('/')),
- openChat: () => _syncContext?.Post(_ => OnOpenWebUI(null, EventArgs.Empty), null),
- openSettings: () => _syncContext?.Post(_ => OnSettings(null, EventArgs.Empty), null),
- openQuickSend: msg => _syncContext?.Post(_ => ShowQuickSendWithMessage(msg), null)
- );
- }
-
- private void ShowQuickSendWithMessage(string? prefill)
- {
- using var dialog = new QuickSendDialog();
- if (!string.IsNullOrEmpty(prefill))
- {
- dialog.SetMessage(prefill);
- }
- if (dialog.ShowDialog() == DialogResult.OK && !string.IsNullOrWhiteSpace(dialog.Message))
- {
- _ = _gatewayClient?.SendChatMessageAsync(dialog.Message);
- }
- }
-
- // --- Event Handlers (marshal to UI thread) ---
-
- private void OnStatusChanged(object? sender, ConnectionStatus status)
- {
- _syncContext?.Post(_ => UpdateStatus(status), null);
- }
-
- private void OnNotificationReceived(object? sender, OpenClawNotification n)
- {
- _syncContext?.Post(_ => ShowNotificationToast(n.Title, n.Message, n.Type), null);
- }
-
- private void OnActivityChanged(object? sender, AgentActivity activity)
- {
- _syncContext?.Post(_ => UpdateActivity(activity), null);
- }
-
- private void OnChannelHealthUpdated(object? sender, ChannelHealth[] channels)
- {
- _lastCheckTime = DateTime.Now;
- _syncContext?.Post(_ => UpdateChannelHealth(channels), null);
- }
-
- private void OnSessionsUpdated(object? sender, SessionInfo[] sessions)
- {
- _lastCheckTime = DateTime.Now;
- _syncContext?.Post(_ => UpdateSessions(sessions), null);
- }
-
- private void OnUsageUpdated(object? sender, GatewayUsageInfo usage)
- {
- _syncContext?.Post(_ => UpdateUsage(usage), null);
- }
-
- // --- UI Updates ---
-
- private void UpdateStatus(ConnectionStatus status)
- {
- _currentStatus = status;
-
- if (_notifyIcon != null)
- {
- var oldIcon = _notifyIcon.Icon;
- _notifyIcon.Icon = CreateStatusIcon(status, _currentActivity?.Kind);
- SafeDestroyIcon(oldIcon);
-
- var tooltip = _currentActivity?.Kind != ActivityKind.Idle && !string.IsNullOrEmpty(_currentActivity?.DisplayText)
- ? $"OpenClaw — {_currentActivity.DisplayText}"
- : $"OpenClaw — {status}";
-
- // Add last check time (culture-aware)
- var checkTime = _lastCheckTime.ToShortTimeString();
- tooltip = $"{tooltip}\nLast check: {checkTime}";
-
- _notifyIcon.Text = tooltip.Length > 63 ? tooltip[..63] : tooltip;
- }
-
- if (_statusItem != null)
- {
- var label = status switch
- {
- ConnectionStatus.Connected => "[ON]",
- ConnectionStatus.Connecting => "[...]",
- ConnectionStatus.Error => "[ERR]",
- _ => "[OFF]"
- };
- _statusItem.Text = $"{label} Gateway: {status}";
- }
- }
-
- private void UpdateActivity(AgentActivity activity)
- {
- // Track per-session activity for stable display
- _sessionActivities[activity.SessionKey] = activity;
-
- // Resolve which session to display using stable selection:
- // 1. Active main session always wins
- // 2. Keep current session if still active (prevents flip-flop)
- // 3. Fall back to most recently active non-main session
- var displayActivity = ResolveDisplayActivity(activity);
- _currentActivity = displayActivity;
-
- if (_activityItem != null)
- {
- if (displayActivity.Kind != ActivityKind.Idle && !string.IsNullOrEmpty(displayActivity.DisplayText))
- {
- _activityItem.Text = displayActivity.DisplayText;
- _activityItem.Visible = true;
- }
- else
- {
- _activityItem.Visible = false;
- }
- }
-
- // Also update the tray icon to reflect activity
- UpdateStatus(_currentStatus);
- }
-
- ///
- /// Selects the best session to display in the activity row.
- /// Avoids rapid switching between sessions by applying a debounce window.
- ///
- private AgentActivity ResolveDisplayActivity(AgentActivity incoming)
- {
- var now = DateTime.UtcNow;
-
- // If main session is active, always prefer it
- if (incoming.IsMain && incoming.Kind != ActivityKind.Idle)
- {
- _displayedSessionKey = incoming.SessionKey;
- _lastSessionSwitch = now;
- return incoming;
- }
-
- // If the currently displayed session is still active, keep it (no flip-flop)
- if (_displayedSessionKey != null &&
- _sessionActivities.TryGetValue(_displayedSessionKey, out var current) &&
- current.Kind != ActivityKind.Idle)
- {
- // Only allow switching away if debounce period has passed
- if (now - _lastSessionSwitch < SessionSwitchDebounce)
- return current;
- }
-
- // Check if any main session is active
- foreach (var kvp in _sessionActivities)
- {
- if (kvp.Value.IsMain && kvp.Value.Kind != ActivityKind.Idle)
- {
- _displayedSessionKey = kvp.Key;
- _lastSessionSwitch = now;
- return kvp.Value;
- }
- }
-
- // No main active — show the incoming active session if it has work
- if (incoming.Kind != ActivityKind.Idle)
- {
- _displayedSessionKey = incoming.SessionKey;
- _lastSessionSwitch = now;
- return incoming;
- }
-
- // Everything is idle
- _displayedSessionKey = null;
- return incoming;
- }
-
- private void UpdateChannelHealth(ChannelHealth[] channels)
- {
- // Store for modern menu
- _lastChannels = channels;
-
- // Remove old channel items
- foreach (var item in _channelItems)
- _contextMenu?.Items.Remove(item);
- _channelItems.Clear();
-
- if (channels.Length == 0)
- {
- if (_channelSeparator != null) _channelSeparator.Visible = false;
- return;
- }
-
- if (_channelSeparator != null) _channelSeparator.Visible = true;
-
- var insertIndex = _contextMenu?.Items.IndexOf(_channelSeparator!) ?? -1;
- if (insertIndex < 0) return;
-
- // Add header
- insertIndex++;
- var header = new ToolStripMenuItem("📡 Channels") { Enabled = false };
- _contextMenu!.Items.Insert(insertIndex, header);
- _channelItems.Add(header);
-
- foreach (var ch in channels)
- {
- insertIndex++;
- var item = new ToolStripMenuItem($" {ch.DisplayText}") { Enabled = false };
- _contextMenu.Items.Insert(insertIndex, item);
- _channelItems.Add(item);
- }
- }
-
- private void UpdateSessions(SessionInfo[] sessions)
- {
- // Store for modern menu
- _lastSessions = sessions;
-
- // Log session data for debugging
- Logger.Info($"UpdateSessions: {sessions.Length} sessions");
- foreach (var s in sessions)
- Logger.Info($" Session: key={s.Key}, isMain={s.IsMain}, status={s.Status}, channel={s.Channel}");
-
- // Remove old session items
- foreach (var item in _sessionItems)
- _contextMenu?.Items.Remove(item);
- _sessionItems.Clear();
-
- if (sessions.Length == 0)
- {
- if (_sessionSeparator != null) _sessionSeparator.Visible = false;
- return;
- }
-
- if (_sessionSeparator != null) _sessionSeparator.Visible = true;
-
- var insertIndex = _contextMenu?.Items.IndexOf(_sessionSeparator!) ?? -1;
- if (insertIndex < 0) return;
-
- // Add header
- insertIndex++;
- var header = new ToolStripMenuItem("🧠 Sessions") { Enabled = false };
- _contextMenu!.Items.Insert(insertIndex, header);
- _sessionItems.Add(header);
-
- foreach (var session in sessions)
- {
- insertIndex++;
- var displayText = session.RichDisplayText;
- if (!string.IsNullOrWhiteSpace(session.AgeText))
- displayText += $" · {session.AgeText}";
- var item = new ToolStripMenuItem($" {displayText}") { Enabled = false };
- _contextMenu.Items.Insert(insertIndex, item);
- _sessionItems.Add(item);
- }
- }
-
- private void UpdateUsage(GatewayUsageInfo usage)
- {
- // Store for modern menu
- _lastUsage = usage;
-
- if (_usageItem != null)
- {
- _usageItem.Text = $"📊 {usage.DisplayText}";
- _usageItem.Visible = true;
- }
- }
-
- // --- Icon Creation (with proper cleanup) ---
-
- private Icon CreateStatusIcon(ConnectionStatus status, ActivityKind? activity = null)
- {
- var bitmap = new Bitmap(16, 16);
- using (var g = Graphics.FromImage(bitmap))
- {
- g.Clear(Color.Transparent);
-
- if (status == ConnectionStatus.Connected)
- {
- // Draw pixel lobster when connected
- DrawPixelLobster(g);
- }
- else
- {
- g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
-
- // Base color from status
- var baseColor = status switch
- {
- ConnectionStatus.Connecting => Color.FromArgb(255, 180, 0), // Amber
- ConnectionStatus.Error => Color.FromArgb(220, 50, 50), // Red
- _ => Color.FromArgb(128, 128, 128) // Gray
- };
-
- // Main circle for non-connected states
- using var brush = new SolidBrush(baseColor);
- g.FillEllipse(brush, 1, 1, 13, 13);
- }
-
- // Activity badge (small dot in corner when working)
- if (activity is not null and not ActivityKind.Idle && status == ConnectionStatus.Connected)
- {
- var badgeColor = activity switch
- {
- ActivityKind.Exec => Color.FromArgb(255, 100, 0), // Orange
- ActivityKind.Write or ActivityKind.Edit => Color.FromArgb(100, 200, 50), // Green
- ActivityKind.Read => Color.FromArgb(80, 150, 255), // Blue
- ActivityKind.Search or ActivityKind.Browser => Color.FromArgb(180, 80, 255), // Purple
- ActivityKind.Message => Color.FromArgb(50, 200, 100), // Bright green
- _ => Color.White
- };
- using var badgeBrush = new SolidBrush(badgeColor);
- g.FillEllipse(badgeBrush, 10, 0, 6, 6);
- using var borderPen = new Pen(Color.Black, 1);
- g.DrawEllipse(borderPen, 10, 0, 6, 6);
- }
- }
-
- var hIcon = bitmap.GetHicon();
- var icon = Icon.FromHandle(hIcon);
- bitmap.Dispose();
- return icon;
- }
-
- private 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)
- bitmap_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)
- bitmap_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)
- bitmap_SetPixel(g, x, y, claw);
-
- // Eyes
- bitmap_SetPixel(g, 6, 4, eyeLight);
- bitmap_SetPixel(g, 9, 4, eyeLight);
- bitmap_SetPixel(g, 6, 5, eyeDark);
- bitmap_SetPixel(g, 9, 5, eyeDark);
- }
-
- private void bitmap_SetPixel(Graphics g, int x, int y, Color c)
- {
- using var brush = new SolidBrush(c);
- g.FillRectangle(brush, x, y, 1, 1);
- }
-
- private static void SafeDestroyIcon(Icon? icon)
- {
- if (icon == null) return;
- try
- {
- DestroyIcon(icon.Handle);
- icon.Dispose();
- }
- catch { }
- }
-
- // --- Toast Notifications ---
-
- private void ShowNotificationToast(string title, string message, string type = "info")
- {
- // Always log to history regardless of filter
- NotificationHistoryForm.AddEntry(title, message, type);
-
- // Check per-type filter
- if (_settings?.ShouldNotify(type) != true) return;
-
- try
- {
- new ToastContentBuilder()
- .AddText(title)
- .AddText(message)
- .Show();
- }
- catch (Exception)
- {
- _notifyIcon?.ShowBalloonTip(3000, title, message, ToolTipIcon.Info);
- }
- }
-
- private void ShowErrorToast(string title, string message)
- {
- try
- {
- new ToastContentBuilder()
- .AddText(title)
- .AddText(message)
- .Show();
- }
- catch
- {
- _notifyIcon?.ShowBalloonTip(3000, title, message, ToolTipIcon.Error);
- }
- }
-
- private void OnToastActivated(ToastNotificationActivatedEventArgsCompat e)
- {
- // Parse arguments from toast
- var args = ToastArguments.Parse(e.Argument);
-
- if (args.TryGetValue("action", out var action) && action == "openDashboard")
- {
- if (args.TryGetValue("url", out var url))
- {
- try
- {
- Process.Start(new ProcessStartInfo(url) { UseShellExecute = true });
- }
- catch (Exception ex)
- {
- Logger.Error($"Failed to open dashboard from toast: {ex.Message}");
- }
- }
- }
- }
-
- // --- Menu Actions ---
-
- private async void OnHealthCheck(object? sender, EventArgs e)
- {
- if (_gatewayClient != null && _currentStatus != ConnectionStatus.Connecting)
- await _gatewayClient.CheckHealthAsync();
- }
-
- private async void OnSessionPoll(object? sender, EventArgs e)
- {
- if (_gatewayClient != null && _currentStatus == ConnectionStatus.Connected)
- {
- await _gatewayClient.RequestSessionsAsync();
- await _gatewayClient.RequestUsageAsync();
- }
- }
-
- private async void OnManualHealthCheck(object? sender, EventArgs e)
- {
- Logger.Info("Manual health check triggered");
- if (_gatewayClient != null)
- {
- await _gatewayClient.CheckHealthAsync();
- await _gatewayClient.RequestSessionsAsync();
- await _gatewayClient.RequestUsageAsync();
- }
- }
-
- private void OnOpenWebUI(object? sender, EventArgs e)
- {
- try
- {
- WebChatForm.ShowOrFocus(_settings!.GatewayUrl, _settings.Token);
- }
- catch (Exception ex)
- {
- // Fallback to browser if WebView2 fails
- Logger.Warn($"WebView2 failed, falling back to browser: {ex.Message}");
- var url = _settings!.GatewayUrl
- .Replace("ws://", "http://")
- .Replace("wss://", "https://");
- try
- {
- Process.Start(new ProcessStartInfo(url) { UseShellExecute = true });
- }
- catch (Exception ex2)
- {
- ShowErrorToast("Failed to open Web UI", ex2.Message);
- }
- }
- }
-
- private string GetDashboardUrl()
- {
- var baseUrl = _settings!.GatewayUrl
- .Replace("ws://", "http://")
- .Replace("wss://", "https://");
-
- // Add token if available
- if (!string.IsNullOrEmpty(_settings.Token))
- {
- var separator = baseUrl.Contains("?") ? "&" : "?";
- return $"{baseUrl}{separator}token={Uri.EscapeDataString(_settings.Token)}";
- }
- return baseUrl;
- }
-
- private void OnOpenDashboard(object? sender, EventArgs e)
- {
- try
- {
- Process.Start(new ProcessStartInfo(GetDashboardUrl()) { UseShellExecute = true });
- }
- catch (Exception ex)
- {
- ShowErrorToast("Failed to open Dashboard", ex.Message);
- }
- }
-
- private async void OnQuickSend(object? sender, EventArgs e)
- {
- using var dialog = new QuickSendDialog();
- if (dialog.ShowDialog() == DialogResult.OK)
- {
- try
- {
- await _gatewayClient!.SendChatMessageAsync(dialog.Message);
- ShowClickableToast("Message Sent", "Click to continue chat in dashboard");
- }
- catch (Exception ex)
- {
- ShowErrorToast("Failed to Send", ex.Message);
- }
- }
- }
-
- private void ShowClickableToast(string title, string message)
- {
- NotificationHistoryForm.AddEntry(title, message, "info");
-
- try
- {
- new ToastContentBuilder()
- .AddText(title)
- .AddText(message)
- .AddArgument("action", "openDashboard")
- .AddArgument("url", GetDashboardUrl())
- .Show();
- }
- catch
- {
- _notifyIcon?.ShowBalloonTip(3000, title, message, ToolTipIcon.Info);
- }
- }
-
- private void OnSettings(object? sender, EventArgs e)
- {
- using var dialog = new SettingsDialog(_settings!);
- if (dialog.ShowDialog() == DialogResult.OK)
- {
- _settings!.Save();
- Task.Run(async () => await ReconnectAsync());
- }
- }
-
- private void ShowFirstRunWelcome()
- {
- var dashboardUrl = _settings!.GatewayUrl
- .Replace("ws://", "http://")
- .Replace("wss://", "https://");
-
- using var welcome = new WelcomeDialog(dashboardUrl);
- if (welcome.ShowDialog() == DialogResult.OK)
- {
- // User clicked "Open Settings"
- using var settings = new SettingsDialog(_settings);
- if (settings.ShowDialog() == DialogResult.OK)
- {
- _settings.Save();
- }
- }
- }
-
- private void OnToggleAutoStart(object? sender, EventArgs e)
- {
- var menuItem = (ToolStripMenuItem)sender!;
- _settings!.AutoStart = !_settings.AutoStart;
- menuItem.Checked = _settings.AutoStart;
- _settings.Save();
- AutoStartManager.SetAutoStart(_settings.AutoStart);
- Logger.Info($"Auto-start: {_settings.AutoStart}");
- }
-
- private void OnShowStatusDetail(object? sender, EventArgs e)
- {
- StatusDetailForm.ShowOrFocus(_gatewayClient, _settings, _currentStatus);
- }
-
- private void OnNotificationHistory(object? sender, EventArgs e)
- {
- NotificationHistoryForm.ShowOrFocus();
- }
-
- private void OnOpenLogFile(object? sender, EventArgs e)
- {
- try
- {
- var logDir = System.IO.Path.Combine(
- Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
- "OpenClawTray");
- var logPath = System.IO.Path.Combine(logDir, "openclaw-tray.log");
-
- if (System.IO.File.Exists(logPath))
- {
- Process.Start(new ProcessStartInfo(logPath) { UseShellExecute = true });
- }
- else
- {
- Process.Start(new ProcessStartInfo(logDir) { UseShellExecute = true });
- }
- }
- catch (Exception ex)
- {
- ShowErrorToast("Failed to Open Log", ex.Message);
- }
- }
-
- private async Task ReconnectAsync()
- {
- try
- {
- if (_gatewayClient != null)
- {
- await _gatewayClient.DisconnectAsync();
- _gatewayClient.Dispose();
- }
-
- _gatewayClient = new OpenClawGatewayClient(_settings!.GatewayUrl, _settings.Token, Logger.Instance);
- _gatewayClient.StatusChanged += OnStatusChanged;
- _gatewayClient.NotificationReceived += OnNotificationReceived;
- _gatewayClient.ActivityChanged += OnActivityChanged;
- _gatewayClient.ChannelHealthUpdated += OnChannelHealthUpdated;
- _gatewayClient.SessionsUpdated += OnSessionsUpdated;
- _gatewayClient.UsageUpdated += OnUsageUpdated;
-
- await _gatewayClient.ConnectAsync();
- }
- catch (Exception ex)
- {
- Logger.Error("Reconnection failed", ex);
- ShowErrorToast("Reconnection Failed", ex.Message);
- }
- }
-
- private void OnDoubleClick(object? sender, EventArgs e) => OnOpenWebUI(sender, e);
- private void OnExit(object? sender, EventArgs e) => ExitThread();
-
- // --- Cleanup ---
-
- protected override void Dispose(bool disposing)
- {
- if (disposing)
- {
- Logger.Info("Application shutting down");
- _globalHotkey?.Dispose();
- _healthCheckTimer?.Dispose();
- _sessionPollTimer?.Dispose();
- _gatewayClient?.Dispose();
- _modernMenu?.Dispose();
- _notifyIcon?.Dispose();
- _contextMenu?.Dispose();
- Logger.Shutdown();
- }
- base.Dispose(disposing);
- }
-
- protected override void ExitThreadCore()
- {
- if (_notifyIcon != null) _notifyIcon.Visible = false;
- base.ExitThreadCore();
- }
-}
diff --git a/src/OpenClaw.Tray/UpdateDialog.cs b/src/OpenClaw.Tray/UpdateDialog.cs
deleted file mode 100644
index 862d975..0000000
--- a/src/OpenClaw.Tray/UpdateDialog.cs
+++ /dev/null
@@ -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;
- }
-}
-
diff --git a/src/OpenClaw.Tray/WebChatForm.cs b/src/OpenClaw.Tray/WebChatForm.cs
deleted file mode 100644
index ae1522b..0000000
--- a/src/OpenClaw.Tray/WebChatForm.cs
+++ /dev/null
@@ -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;
-
-///
-/// Embeds the OpenClaw WebChat UI via WebView2 with modern Windows 11 styling.
-///
-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);
- }
-}
-
-
diff --git a/src/OpenClaw.Tray/WelcomeDialog.cs b/src/OpenClaw.Tray/WelcomeDialog.cs
deleted file mode 100644
index cf2616d..0000000
--- a/src/OpenClaw.Tray/WelcomeDialog.cs
+++ /dev/null
@@ -1,95 +0,0 @@
-using System;
-using System.Diagnostics;
-using System.Drawing;
-using System.Windows.Forms;
-
-namespace OpenClawTray;
-
-///
-/// First-run welcome dialog to help users get started with OpenClaw.
-///
-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 });
- }
-}
diff --git a/src/OpenClaw.Tray/openclaw.ico b/src/OpenClaw.Tray/openclaw.ico
deleted file mode 100644
index 1b7300e..0000000
Binary files a/src/OpenClaw.Tray/openclaw.ico and /dev/null differ
diff --git a/src/OpenClaw.Tray/screenshots/README.md b/src/OpenClaw.Tray/screenshots/README.md
deleted file mode 100644
index 33e630e..0000000
--- a/src/OpenClaw.Tray/screenshots/README.md
+++ /dev/null
@@ -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
\ No newline at end of file