diff --git a/README.md b/README.md
index 8e2ade8..5677fee 100644
--- a/README.md
+++ b/README.md
@@ -2,6 +2,8 @@
A Windows companion suite for [Moltbot](https://moltbot.com) - the AI-powered personal assistant.
+
+
## Projects
This monorepo contains three projects:
@@ -29,19 +31,30 @@ dotnet build
dotnet run --project src/Moltbot.Tray
```
-## ๐ฆ Moltbot.Tray
+## ๐ฆ Moltbot.Tray (Molty)
-Windows system tray companion that connects to your local Moltbot gateway.
+Modern Windows 11-style system tray companion that connects to your local Moltbot gateway.
### Features
-- ๐ฆ Lobster icon in system tray (connected/disconnected states)
-- ๐ฌ Quick Send - Send messages via global hotkey (Ctrl+Alt+Shift+C)
-- ๐ Auto-updates from GitHub Releases
-- ๐ Web Chat - Embedded chat window
-- ๐ Status Display - View sessions and channels
-- ๐ Toast Notifications - Clickable Windows notifications
-- ๐ Auto-start with Windows
-- โ๏ธ Settings management
+- ๐ฆ **Lobster branding** - Pixel-art lobster tray icon with status colors
+- ๐จ **Modern UI** - Windows 11 flyout menu with dark/light mode support
+- ๐ฌ **Quick Send** - Send messages via global hotkey (Ctrl+Alt+Shift+C)
+- ๐ **Auto-updates** - Automatic updates from GitHub Releases
+- ๐ **Web Chat** - Embedded chat window with WebView2
+- ๐ **Live Status** - Real-time sessions, channels, and usage display
+- ๐ **Toast Notifications** - Clickable Windows notifications with filters
+- ๐ก **Channel Control** - Start/stop Telegram & WhatsApp from the menu
+- โฑ **Cron Jobs** - Quick access to scheduled tasks
+- ๐ **Auto-start** - Launch with Windows
+- โ๏ธ **Settings** - Full configuration dialog
+- ๐ฏ **First-run experience** - Welcome dialog guides new users
+
+### Menu Sections
+- **Status** - Gateway connection status with click-to-view details
+- **Sessions** - Active agent sessions (clickable โ dashboard)
+- **Channels** - Telegram/WhatsApp status with toggle control
+- **Actions** - Dashboard, Web Chat, Quick Send, Cron Jobs, History
+- **Settings** - Configuration, auto-start, logs
### Mac Parity Status
@@ -55,6 +68,9 @@ Windows system tray companion that connects to your local Moltbot gateway.
| Auto-start | โ
| โ
|
| Session display | โ
| โ
|
| Channel health | โ
| โ
|
+| Channel control | โ
| โ
|
+| Modern UI styling | โ
| โ
|
+| Dark/Light mode | โ
| โ
|
| Deep links | โ
| ๐ |
## ๐ฆ Moltbot.CommandPalette
@@ -81,6 +97,7 @@ Shared library containing:
- `MoltbotGatewayClient` - WebSocket client for gateway protocol
- `IMoltbotLogger` - Logging interface
- Data models (SessionInfo, ChannelHealth, etc.)
+- Channel control (start/stop channels via gateway)
## Development
@@ -91,6 +108,8 @@ moltbot-windows-hub/
โ โโโ Moltbot.Shared/ # Shared gateway library
โ โโโ Moltbot.Tray/ # System tray app
โ โโโ Moltbot.CommandPalette/ # PowerToys extension
+โโโ docs/
+โ โโโ molty1.png # Screenshot
โโโ moltbot-windows-hub.sln
โโโ README.md
โโโ LICENSE
@@ -105,6 +124,13 @@ Settings are stored in:
Default gateway: `ws://localhost:18789`
+### First Run
+
+On first run without a token, Molty displays a welcome dialog that:
+1. Explains what's needed to get started
+2. Links to [documentation](https://docs.molt.bot/web/dashboard) for token setup
+3. Opens Settings to configure the connection
+
## License
MIT License - see [LICENSE](LICENSE)
diff --git a/docs/molty1.png b/docs/molty1.png
new file mode 100644
index 0000000..034b544
Binary files /dev/null and b/docs/molty1.png differ
diff --git a/src/Moltbot.Shared/MoltbotGatewayClient.cs b/src/Moltbot.Shared/MoltbotGatewayClient.cs
index 527f178..4222be9 100644
--- a/src/Moltbot.Shared/MoltbotGatewayClient.cs
+++ b/src/Moltbot.Shared/MoltbotGatewayClient.cs
@@ -163,6 +163,54 @@ public class MoltbotGatewayClient : IDisposable
catch { }
}
+ /// Start a channel (telegram, whatsapp, etc).
+ public async Task StartChannelAsync(string channelName)
+ {
+ if (_webSocket?.State != WebSocketState.Open) return false;
+ try
+ {
+ var req = new
+ {
+ type = "req",
+ id = Guid.NewGuid().ToString(),
+ method = "channel.start",
+ @params = new { channel = channelName }
+ };
+ await SendRawAsync(JsonSerializer.Serialize(req));
+ _logger.Info($"Sent channel.start for {channelName}");
+ return true;
+ }
+ catch (Exception ex)
+ {
+ _logger.Error($"Failed to start channel {channelName}", ex);
+ return false;
+ }
+ }
+
+ /// Stop a channel (telegram, whatsapp, etc).
+ public async Task StopChannelAsync(string channelName)
+ {
+ if (_webSocket?.State != WebSocketState.Open) return false;
+ try
+ {
+ var req = new
+ {
+ type = "req",
+ id = Guid.NewGuid().ToString(),
+ method = "channel.stop",
+ @params = new { channel = channelName }
+ };
+ await SendRawAsync(JsonSerializer.Serialize(req));
+ _logger.Info($"Sent channel.stop for {channelName}");
+ return true;
+ }
+ catch (Exception ex)
+ {
+ _logger.Error($"Failed to stop channel {channelName}", ex);
+ return false;
+ }
+ }
+
// --- Connection management ---
private async Task ReconnectWithBackoffAsync()
@@ -594,6 +642,8 @@ public class MoltbotGatewayClient : IDisposable
bool isConfigured = false;
bool isLinked = false;
bool probeOk = false;
+ bool hasError = false;
+ string? tokenSource = null;
if (val.TryGetProperty("running", out var running))
isRunning = running.GetBoolean();
@@ -607,18 +657,27 @@ public class MoltbotGatewayClient : IDisposable
// Check probe status for webhook-based channels like Telegram
if (val.TryGetProperty("probe", out var probe) && probe.TryGetProperty("ok", out var ok))
probeOk = ok.GetBoolean();
+ // Check for errors
+ if (val.TryGetProperty("lastError", out var lastError) && lastError.ValueKind != JsonValueKind.Null)
+ hasError = true;
+ // Check token source (for Telegram - if configured, bot token was validated)
+ if (val.TryGetProperty("tokenSource", out var ts))
+ tokenSource = ts.GetString();
- // Determine status string
+ // Determine status string - unified for parity between channels
+ // Key insight: if configured=true and no errors, the channel is ready
+ // - WhatsApp: linked=true means authenticated
+ // - Telegram: configured=true means bot token was validated
if (val.TryGetProperty("status", out var status))
ch.Status = status.GetString() ?? "unknown";
+ else if (hasError)
+ ch.Status = "error";
else if (isRunning)
ch.Status = "running";
- else if (probeOk && isConfigured)
- ch.Status = "ready"; // Webhook mode, bot is responding
- else if (isLinked)
- ch.Status = "linked"; // Authenticated but not running
- else if (isConfigured)
- ch.Status = "stopped";
+ else if (isConfigured && (probeOk || isLinked))
+ ch.Status = "ready"; // Explicitly verified ready
+ else if (isConfigured && !hasError)
+ ch.Status = "ready"; // Configured without errors = ready (token was validated at config time)
else
ch.Status = "not configured";
diff --git a/src/Moltbot.Tray/DownloadProgressDialog.cs b/src/Moltbot.Tray/DownloadProgressDialog.cs
index b8fd8a8..b4c3cfa 100644
--- a/src/Moltbot.Tray/DownloadProgressDialog.cs
+++ b/src/Moltbot.Tray/DownloadProgressDialog.cs
@@ -6,7 +6,7 @@ using Updatum;
namespace MoltbotTray;
-public class DownloadProgressDialog : Form
+public class DownloadProgressDialog : ModernForm
{
private readonly UpdatumManager _updater;
private readonly ProgressBar _progressBar;
@@ -17,41 +17,26 @@ public class DownloadProgressDialog : Form
_updater = updater;
_updater.PropertyChanged += UpdaterOnPropertyChanged;
- Text = "Downloading Update - Moltbot Tray";
- Size = new Size(400, 150);
- StartPosition = FormStartPosition.CenterScreen;
- FormBorderStyle = FormBorderStyle.FixedDialog;
- MaximizeBox = false;
- MinimizeBox = false;
- ControlBox = false; // No close button during download
- Icon = SystemIcons.Information;
+ Text = "Downloading Update โ Moltbot Tray";
+ Size = new Size(420, 160);
+ ControlBox = false;
+ Icon = IconHelper.GetLobsterIcon();
- var titleLabel = new Label
- {
- Text = "๐ฆ Downloading update...",
- Font = new Font(Font.FontFamily, 10, FontStyle.Bold),
- Location = new Point(20, 20),
- AutoSize = true
- };
+ 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 = new ProgressBar
- {
- Location = new Point(20, 55),
- Size = new Size(340, 25),
- Minimum = 0,
- Maximum = 100,
- Style = ProgressBarStyle.Continuous
- };
+ _progressBar = CreateModernProgressBar();
+ _progressBar.Location = new Point(20, 60);
+ _progressBar.Size = new Size(364, 8);
Controls.Add(_progressBar);
- _progressLabel = new Label
- {
- Text = "Starting download...",
- Location = new Point(20, 85),
- Size = new Size(340, 20),
- TextAlign = ContentAlignment.MiddleCenter
- };
+ _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);
}
@@ -60,13 +45,9 @@ public class DownloadProgressDialog : Form
if (e.PropertyName == nameof(UpdatumManager.DownloadedPercentage))
{
if (InvokeRequired)
- {
Invoke(() => UpdateProgress());
- }
else
- {
UpdateProgress();
- }
}
}
@@ -82,3 +63,4 @@ public class DownloadProgressDialog : Form
base.OnFormClosing(e);
}
}
+
diff --git a/src/Moltbot.Tray/ModernForm.cs b/src/Moltbot.Tray/ModernForm.cs
new file mode 100644
index 0000000..d554977
--- /dev/null
+++ b/src/Moltbot.Tray/ModernForm.cs
@@ -0,0 +1,257 @@
+using System;
+using System.Drawing;
+using System.Drawing.Drawing2D;
+using System.Runtime.InteropServices;
+using System.Windows.Forms;
+using Microsoft.Win32;
+
+namespace MoltbotTray;
+
+///
+/// Base form with Windows 11 modern styling - dark/light mode, rounded corners, Moltbot 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 Moltbot 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/Moltbot.Tray/ModernTrayMenu.cs b/src/Moltbot.Tray/ModernTrayMenu.cs
new file mode 100644
index 0000000..7655bb2
--- /dev/null
+++ b/src/Moltbot.Tray/ModernTrayMenu.cs
@@ -0,0 +1,435 @@
+using System;
+using System.Drawing;
+using System.Drawing.Drawing2D;
+using System.Runtime.InteropServices;
+using System.Windows.Forms;
+using Microsoft.Win32;
+
+namespace MoltbotTray;
+
+///
+/// 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;
+
+ 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)
+ {
+ _items.Add(new ModernMenuItem
+ {
+ Id = "",
+ Icon = icon,
+ Text = text,
+ Enabled = false,
+ IsHeader = true,
+ IsBrandHeader = true,
+ IsSeparator = false
+ });
+ }
+
+ 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;
+
+ 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;
+
+ // 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;
+ break;
+ }
+ }
+ y += itemHeight;
+ }
+
+ 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; }
+ }
+}
diff --git a/src/Moltbot.Tray/NotificationHistoryForm.cs b/src/Moltbot.Tray/NotificationHistoryForm.cs
index 7ffa390..9cac87d 100644
--- a/src/Moltbot.Tray/NotificationHistoryForm.cs
+++ b/src/Moltbot.Tray/NotificationHistoryForm.cs
@@ -6,9 +6,9 @@ using System.Windows.Forms;
namespace MoltbotTray;
///
-/// Shows recent notification history in a simple list view.
+/// Shows recent notification history in a modern styled list view.
///
-public class NotificationHistoryForm : Form
+public class NotificationHistoryForm : ModernForm
{
private ListView? _listView;
private Button _clearButton = null!;
@@ -30,12 +30,10 @@ public class NotificationHistoryForm : Form
Type = type
});
- // Trim old entries
while (_history.Count > MaxHistory)
_history.RemoveAt(0);
}
- // If window is open, refresh it
_instance?.RefreshList();
}
@@ -61,9 +59,9 @@ public class NotificationHistoryForm : Form
private void InitializeComponent()
{
Text = "Notification History โ Moltbot Tray";
- Size = new Size(600, 450);
- MinimumSize = new Size(400, 300);
- StartPosition = FormStartPosition.CenterScreen;
+ Size = new Size(680, 500);
+ MinimumSize = new Size(480, 340);
+ FormBorderStyle = FormBorderStyle.Sizable;
Icon = IconHelper.GetLobsterIcon();
_listView = new ListView
@@ -71,42 +69,49 @@ public class NotificationHistoryForm : Form
Dock = DockStyle.Fill,
View = View.Details,
FullRowSelect = true,
- GridLines = true,
- Font = new Font("Segoe UI", 9F)
+ GridLines = false,
+ Font = new Font("Segoe UI", 9.5F),
+ BackColor = SurfaceColor,
+ ForeColor = ForegroundColor,
+ BorderStyle = BorderStyle.None
};
- _listView.Columns.Add("Time", 130);
- _listView.Columns.Add("Type", 80);
- _listView.Columns.Add("Title", 150);
- _listView.Columns.Add("Message", 300);
+ _listView.Columns.Add("Time", 140);
+ _listView.Columns.Add("Type", 85);
+ _listView.Columns.Add("Title", 160);
+ _listView.Columns.Add("Message", 320);
- var buttonPanel = new FlowLayoutPanel
+ var buttonPanel = new Panel
{
Dock = DockStyle.Bottom,
- Height = 40,
- FlowDirection = FlowDirection.RightToLeft,
- Padding = new Padding(5)
+ Height = 56,
+ BackColor = SurfaceColor,
+ Padding = new Padding(16, 12, 16, 12)
};
- _closeButton = new Button
- {
- Text = "&Close",
- Size = new Size(75, 26),
- Font = new Font("Segoe UI", 9F)
- };
+ _closeButton = CreateModernButton("Close");
+ _closeButton.Size = new Size(90, 36);
_closeButton.Click += (_, _) => Close();
- _clearButton = new Button
- {
- Text = "C&lear All",
- Size = new Size(85, 26),
- Font = new Font("Segoe UI", 9F)
- };
+ _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);
@@ -129,7 +134,6 @@ public class NotificationHistoryForm : Form
lock (_history)
{
- // Show newest first
for (int i = _history.Count - 1; i >= 0; i--)
{
var entry = _history[i];
@@ -159,3 +163,4 @@ public class NotificationHistoryForm : Form
}
}
+
diff --git a/src/Moltbot.Tray/QuickSendDialog.cs b/src/Moltbot.Tray/QuickSendDialog.cs
index efc8ae7..b4dd2b3 100644
--- a/src/Moltbot.Tray/QuickSendDialog.cs
+++ b/src/Moltbot.Tray/QuickSendDialog.cs
@@ -4,7 +4,7 @@ using System.Windows.Forms;
namespace MoltbotTray;
-public partial class QuickSendDialog : Form
+public partial class QuickSendDialog : ModernForm
{
private TextBox _messageTextBox = null!;
private Button _sendButton = null!;
@@ -20,85 +20,52 @@ public partial class QuickSendDialog : Form
private void InitializeComponent()
{
- // Form properties
Text = "Quick Send โ Moltbot";
- Size = new Size(500, 220);
- StartPosition = FormStartPosition.CenterScreen;
- FormBorderStyle = FormBorderStyle.FixedDialog;
- MaximizeBox = false;
- MinimizeBox = false;
+ Size = new Size(520, 300);
ShowInTaskbar = true;
- TopMost = true; // Always on top when opened via hotkey
+ TopMost = true;
Icon = IconHelper.GetLobsterIcon();
- // Label
- var label = new Label
- {
- Text = "Send a message to Moltbot:",
- Location = new Point(12, 12),
- Size = new Size(460, 20),
- Font = new Font("Segoe UI", 9.5F, FontStyle.Regular)
- };
+ // Header label
+ var label = CreateModernLabel("Send a message to Moltbot:");
+ label.Location = new Point(20, 20);
+ label.Font = new Font("Segoe UI", 11F, FontStyle.Bold);
+ label.ForeColor = AccentColor;
// Message text box
- _messageTextBox = new TextBox
- {
- Location = new Point(12, 36),
- Size = new Size(460, 90),
- Multiline = true,
- ScrollBars = ScrollBars.Vertical,
- Font = new Font("Segoe UI", 10F, FontStyle.Regular),
- AcceptsReturn = false // Enter sends, Shift+Enter for newline
- };
+ _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);
- // Hint label
- _hintLabel = new Label
- {
- Text = "Enter to send ยท Esc to cancel ยท Shift+Enter for new line",
- Location = new Point(12, 132),
- Size = new Size(300, 18),
- Font = new Font("Segoe UI", 8F, FontStyle.Regular),
- ForeColor = Color.Gray
- };
-
- // Send button
- _sendButton = new Button
- {
- Text = "&Send",
- Location = new Point(316, 148),
- Size = new Size(75, 28),
- UseVisualStyleBackColor = true,
- Font = new Font("Segoe UI", 9F, FontStyle.Regular)
- };
+ // 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;
- // Cancel button
- _cancelButton = new Button
- {
- Text = "&Cancel",
- Location = new Point(397, 148),
- Size = new Size(75, 28),
- UseVisualStyleBackColor = true,
- Font = new Font("Segoe UI", 9F, FontStyle.Regular)
- };
+ _cancelButton = CreateModernButton("Cancel");
+ _cancelButton.Location = new Point(296, 172);
+ _cancelButton.Size = new Size(90, 32);
_cancelButton.Click += OnCancelClick;
- // Set dialog buttons
+ // 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;
- // Add controls
- Controls.Add(label);
- Controls.Add(_messageTextBox);
- Controls.Add(_hintLabel);
- Controls.Add(_sendButton);
- Controls.Add(_cancelButton);
+ Controls.AddRange(new Control[] { label, _messageTextBox, _sendButton, _cancelButton, _hintLabel });
- // Focus the text box on show
Shown += (_, _) =>
{
_messageTextBox.Focus();
- Activate(); // Ensure window is focused when opened via hotkey
+ Activate();
};
}
@@ -109,7 +76,6 @@ public partial class QuickSendDialog : Form
_messageTextBox.Focus();
return;
}
-
DialogResult = DialogResult.OK;
Close();
}
@@ -122,13 +88,11 @@ public partial class QuickSendDialog : Form
protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
{
- // Ctrl+Enter or Enter (without Shift) as send
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/Moltbot.Tray/SettingsDialog.cs b/src/Moltbot.Tray/SettingsDialog.cs
index 9d194ed..0a69393 100644
--- a/src/Moltbot.Tray/SettingsDialog.cs
+++ b/src/Moltbot.Tray/SettingsDialog.cs
@@ -1,3 +1,4 @@
+using Microsoft.Toolkit.Uwp.Notifications;
using Moltbot.Shared;
using System;
using System.Drawing;
@@ -5,7 +6,7 @@ using System.Windows.Forms;
namespace MoltbotTray;
-public partial class SettingsDialog : Form
+public partial class SettingsDialog : ModernForm
{
private readonly SettingsManager _settings;
@@ -16,6 +17,7 @@ public partial class SettingsDialog : Form
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!;
@@ -42,181 +44,121 @@ public partial class SettingsDialog : Form
{
Text = "Settings โ Moltbot Tray";
Size = new Size(480, 560);
- StartPosition = FormStartPosition.CenterScreen;
- FormBorderStyle = FormBorderStyle.FixedDialog;
- MaximizeBox = false;
- MinimizeBox = false;
ShowInTaskbar = false;
AutoScroll = true;
Icon = IconHelper.GetLobsterIcon();
- var y = 12;
- var labelFont = new Font("Segoe UI", 9F);
- var headerFont = new Font("Segoe UI", 9F, FontStyle.Bold);
+ var y = 16;
// --- Connection Section ---
- var connHeader = new Label
- {
- Text = "CONNECTION",
- Location = new Point(12, y),
- Size = new Size(200, 20),
- Font = headerFont,
- ForeColor = Color.FromArgb(0, 120, 215)
- };
- y += 22;
-
- var gatewayUrlLabel = new Label
- {
- Text = "Gateway URL:",
- Location = new Point(12, y),
- Size = new Size(100, 20),
- Font = labelFont
- };
- y += 22;
-
- _gatewayUrlTextBox = new TextBox
- {
- Location = new Point(12, y),
- Size = new Size(310, 23),
- Font = labelFont
- };
-
- _testConnectionButton = new Button
- {
- Text = "Test",
- Location = new Point(330, y - 1),
- Size = new Size(65, 25),
- Font = labelFont
- };
- _testConnectionButton.Click += OnTestConnection;
- y += 30;
-
- var tokenLabel = new Label
- {
- Text = "Token:",
- Location = new Point(12, y),
- Size = new Size(100, 20),
- Font = labelFont
- };
- y += 22;
-
- _tokenTextBox = new TextBox
- {
- Location = new Point(12, y),
- Size = new Size(310, 23),
- Font = labelFont,
- UseSystemPasswordChar = true
- };
-
- _statusLabel = new Label
- {
- Text = "",
- Location = new Point(330, y + 2),
- Size = new Size(130, 20),
- Font = new Font("Segoe UI", 8F),
- ForeColor = Color.DarkGreen
- };
- y += 35;
-
- // --- Startup Section ---
- var startupHeader = new Label
- {
- Text = "STARTUP",
- Location = new Point(12, y),
- Size = new Size(200, 20),
- Font = headerFont,
- ForeColor = Color.FromArgb(0, 120, 215)
- };
- y += 22;
-
- _autoStartCheckBox = new CheckBox
- {
- Text = "Start automatically with Windows",
- Location = new Point(12, y),
- Size = new Size(280, 22),
- Font = labelFont
- };
+ var connHeader = CreateModernLabel("CONNECTION");
+ connHeader.Font = new Font("Segoe UI", 9F, FontStyle.Bold);
+ connHeader.ForeColor = AccentColor;
+ connHeader.Location = new Point(16, y);
y += 26;
- _globalHotkeyCheckBox = new CheckBox
- {
- Text = "Global hotkey (Ctrl+Alt+Shift+C โ Quick Send)",
- Location = new Point(12, y),
- Size = new Size(340, 22),
- Font = labelFont
- };
- y += 35;
+ 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 = new Label
- {
- Text = "NOTIFICATIONS",
- Location = new Point(12, y),
- Size = new Size(200, 20),
- Font = headerFont,
- ForeColor = Color.FromArgb(0, 120, 215)
- };
- y += 22;
+ 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 = new CheckBox
- {
- Text = "Show desktop notifications",
- Location = new Point(12, y),
- Size = new Size(250, 22),
- Font = labelFont
- };
+ _showNotificationsCheckBox = CreateModernCheckBox("Show desktop notifications");
+ _showNotificationsCheckBox.Location = new Point(16, y);
_showNotificationsCheckBox.CheckedChanged += (_, _) =>
{
_notifyFilterPanel.Enabled = _showNotificationsCheckBox.Checked;
};
- y += 26;
+ y += 28;
- var soundLabel = new Label
- {
- Text = "Sound:",
- Location = new Point(12, y),
- Size = new Size(50, 20),
- Font = labelFont
- };
+ var soundLabel = CreateModernLabel("Sound:");
+ soundLabel.Location = new Point(16, y + 3);
+ soundLabel.AutoSize = true;
_notificationSoundComboBox = new ComboBox
{
- Location = new Point(65, y - 2),
- Size = new Size(140, 23),
+ Location = new Point(80, y),
+ Size = new Size(140, 28),
DropDownStyle = ComboBoxStyle.DropDownList,
- Font = labelFont
+ Font = new Font("Segoe UI", 9.5f),
+ BackColor = SurfaceColor,
+ ForeColor = ForegroundColor,
+ FlatStyle = FlatStyle.Flat
};
_notificationSoundComboBox.Items.AddRange(new[] { "Default", "None", "Critical", "Information" });
- y += 30;
+
+ _testNotificationButton = CreateModernButton("Test");
+ _testNotificationButton.Location = new Point(230, y);
+ _testNotificationButton.Size = new Size(80, 28);
+ _testNotificationButton.Click += OnTestNotification;
+ y += 36;
// Filter panel
- var filterLabel = new Label
- {
- Text = "Show toasts for:",
- Location = new Point(12, y),
- Size = new Size(120, 20),
- Font = labelFont,
- ForeColor = Color.Gray
- };
- y += 22;
+ var filterLabel = CreateModernLabel("Show toasts for:", isSubtle: true);
+ filterLabel.Location = new Point(16, y);
+ y += 24;
_notifyFilterPanel = new Panel
{
- Location = new Point(12, y),
+ Location = new Point(16, y),
Size = new Size(440, 72),
- BorderStyle = BorderStyle.None
+ BorderStyle = BorderStyle.None,
+ BackColor = Color.Transparent
};
// Two columns of filter checkboxes
- var cbFont = new Font("Segoe UI", 8.5F);
- _notifyHealthCb = MakeFilterCb("๐ฉธ Health", 0, 0, cbFont);
- _notifyUrgentCb = MakeFilterCb("๐จ Urgent", 0, 24, cbFont);
- _notifyReminderCb = MakeFilterCb("โฐ Reminders", 0, 48, cbFont);
- _notifyEmailCb = MakeFilterCb("๐ง Email", 150, 0, cbFont);
- _notifyCalendarCb = MakeFilterCb("๐
Calendar", 150, 24, cbFont);
- _notifyBuildCb = MakeFilterCb("๐จ Build/CI", 150, 48, cbFont);
- _notifyStockCb = MakeFilterCb("๐ฆ Stock", 300, 0, cbFont);
- _notifyInfoCb = MakeFilterCb("๐ค General", 300, 24, cbFont);
+ _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[]
{
@@ -225,28 +167,19 @@ public partial class SettingsDialog : Form
_notifyStockCb, _notifyInfoCb
});
- y += 80;
+ y += 90;
// --- Buttons ---
- y += 10;
- _okButton = new Button
- {
- Text = "&OK",
- Location = new Point(300, y),
- Size = new Size(75, 28),
- Font = labelFont
- };
- _okButton.Click += OnOkClick;
-
- _cancelButton = new Button
- {
- Text = "&Cancel",
- Location = new Point(382, y),
- Size = new Size(75, 28),
- Font = labelFont
- };
+ _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;
@@ -256,22 +189,35 @@ public partial class SettingsDialog : Form
connHeader, gatewayUrlLabel, _gatewayUrlTextBox, _testConnectionButton,
tokenLabel, _tokenTextBox, _statusLabel,
startupHeader, _autoStartCheckBox, _globalHotkeyCheckBox,
- notifyHeader, _showNotificationsCheckBox, soundLabel, _notificationSoundComboBox,
+ notifyHeader, _showNotificationsCheckBox, soundLabel, _notificationSoundComboBox, _testNotificationButton,
filterLabel, _notifyFilterPanel,
_okButton, _cancelButton
});
}
- private static CheckBox MakeFilterCb(string text, int x, int y, Font font)
+ private void OnTestNotification(object? sender, EventArgs e)
{
- return new CheckBox
+ try
{
- Text = text,
- Location = new Point(x, y),
- Size = new Size(140, 22),
- Font = font,
- Checked = true
- };
+ new ToastContentBuilder()
+ .AddText("๐ฆ Test Notification")
+ .AddText("This is what a Moltbot 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()
diff --git a/src/Moltbot.Tray/StatusDetailForm.cs b/src/Moltbot.Tray/StatusDetailForm.cs
index 054c3af..87bcee6 100644
--- a/src/Moltbot.Tray/StatusDetailForm.cs
+++ b/src/Moltbot.Tray/StatusDetailForm.cs
@@ -9,7 +9,7 @@ namespace MoltbotTray;
///
/// Shows detailed gateway status, sessions, channels, and usage in a rich view.
///
-public class StatusDetailForm : Form
+public class StatusDetailForm : ModernForm
{
private RichTextBox _textBox = null!;
private Button _refreshButton = null!;
@@ -45,44 +45,39 @@ public class StatusDetailForm : Form
private void InitializeComponent()
{
Text = "Moltbot Status";
- Size = new Size(520, 500);
- MinimumSize = new Size(400, 350);
- StartPosition = FormStartPosition.CenterScreen;
+ 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, FontStyle.Regular, GraphicsUnit.Point),
- BackColor = Color.FromArgb(30, 30, 30),
- ForeColor = Color.FromArgb(220, 220, 220),
+ 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
+ WordWrap = true,
+ Padding = new Padding(8)
};
- var buttonPanel = new FlowLayoutPanel
+ var buttonPanel = new Panel
{
Dock = DockStyle.Bottom,
- Height = 40,
- FlowDirection = FlowDirection.RightToLeft,
- Padding = new Padding(5)
+ Height = 56,
+ BackColor = SurfaceColor,
+ Padding = new Padding(16, 12, 16, 12)
};
- _closeButton = new Button
- {
- Text = "&Close",
- Size = new Size(75, 26),
- Font = new Font("Segoe UI", 9F)
- };
+ _closeButton = CreateModernButton("Close");
+ _closeButton.Size = new Size(90, 36);
+ _closeButton.Anchor = AnchorStyles.Right | AnchorStyles.Top;
_closeButton.Click += (_, _) => Close();
- _refreshButton = new Button
- {
- Text = "&Refresh",
- Size = new Size(75, 26),
- Font = new Font("Segoe UI", 9F)
- };
+ _refreshButton = CreateModernButton("Refresh", isPrimary: true);
+ _refreshButton.Size = new Size(90, 36);
+ _refreshButton.Anchor = AnchorStyles.Right | AnchorStyles.Top;
_refreshButton.Click += async (_, _) =>
{
if (_client != null)
@@ -94,8 +89,18 @@ public class StatusDetailForm : Form
RefreshStatus();
};
- buttonPanel.Controls.Add(_closeButton);
- buttonPanel.Controls.Add(_refreshButton);
+ // 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);
@@ -106,7 +111,7 @@ public class StatusDetailForm : Form
var sb = new StringBuilder();
// Header
- sb.AppendLine("โก MOLTBOT STATUS");
+ sb.AppendLine("๐ฆ MOLTBOT STATUS");
sb.AppendLine(new string('โ', 40));
sb.AppendLine();
@@ -154,7 +159,7 @@ public class StatusDetailForm : Form
sb.AppendLine($" Uptime: {GetUptime()}");
sb.AppendLine();
- // Auto-start
+ // Settings
sb.AppendLine("โ๏ธ SETTINGS");
sb.AppendLine(new string('โ', 40));
sb.AppendLine($" Auto-start: {(_settings?.AutoStart == true ? "โ
" : "โ")}");
@@ -181,3 +186,4 @@ public class StatusDetailForm : Form
}
}
+
diff --git a/src/Moltbot.Tray/TrayApplication.cs b/src/Moltbot.Tray/TrayApplication.cs
index 0c0f7cd..4b0fca7 100644
--- a/src/Moltbot.Tray/TrayApplication.cs
+++ b/src/Moltbot.Tray/TrayApplication.cs
@@ -4,6 +4,7 @@ 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;
@@ -16,6 +17,7 @@ public class TrayApplication : ApplicationContext
{
private NotifyIcon? _notifyIcon;
private ContextMenuStrip? _contextMenu;
+ private ModernTrayMenu? _modernMenu;
private MoltbotGatewayClient? _gatewayClient;
private SettingsManager? _settings;
private System.Windows.Forms.Timer? _healthCheckTimer;
@@ -40,6 +42,11 @@ public class TrayApplication : ApplicationContext
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
@@ -51,14 +58,28 @@ public class TrayApplication : ApplicationContext
_startupArgs = args ?? Array.Empty();
_syncContext = SynchronizationContext.Current ?? new WindowsFormsSynchronizationContext();
Logger.Info("Application starting");
- InitializeComponent();
- InitializeAsync();
+ 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;
@@ -113,14 +134,18 @@ public class TrayApplication : ApplicationContext
_contextMenu.Items.Add("Open Log File", null, OnOpenLogFile);
_contextMenu.Items.Add("Exit", null, OnExit);
- // Tray icon
+ // 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),
- ContextMenuStrip = _contextMenu,
Text = "Moltbot Tray โ Disconnected",
Visible = true
};
+ _notifyIcon.MouseClick += OnTrayIconClick;
_notifyIcon.DoubleClick += OnDoubleClick;
// Health check timer (30s)
@@ -131,12 +156,243 @@ public class TrayApplication : ApplicationContext
_sessionPollTimer = new System.Windows.Forms.Timer { Interval = 60000, Enabled = true };
_sessionPollTimer.Tick += OnSessionPoll;
- // Global hotkey: Ctrl+Shift+Space โ Quick Send
+ // 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");
+
+ // 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 = channel.Status.ToLowerInvariant() is "ok" or "connected" or "running";
+
+ 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
@@ -308,6 +564,9 @@ public class TrayApplication : ApplicationContext
private void UpdateChannelHealth(ChannelHealth[] channels)
{
+ // Store for modern menu
+ _lastChannels = channels;
+
// Remove old channel items
foreach (var item in _channelItems)
_contextMenu?.Items.Remove(item);
@@ -341,6 +600,9 @@ public class TrayApplication : ApplicationContext
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)
@@ -383,6 +645,9 @@ public class TrayApplication : ApplicationContext
private void UpdateUsage(GatewayUsageInfo usage)
{
+ // Store for modern menu
+ _lastUsage = usage;
+
if (_usageItem != null)
{
_usageItem.Text = $"๐ {usage.DisplayText}";
@@ -704,6 +969,24 @@ public class TrayApplication : ApplicationContext
}
}
+ 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!;
@@ -789,6 +1072,7 @@ public class TrayApplication : ApplicationContext
_healthCheckTimer?.Dispose();
_sessionPollTimer?.Dispose();
_gatewayClient?.Dispose();
+ _modernMenu?.Dispose();
_notifyIcon?.Dispose();
_contextMenu?.Dispose();
Logger.Shutdown();
@@ -802,5 +1086,3 @@ public class TrayApplication : ApplicationContext
base.ExitThreadCore();
}
}
-
-
diff --git a/src/Moltbot.Tray/UpdateDialog.cs b/src/Moltbot.Tray/UpdateDialog.cs
index 63902c3..bfd0bb0 100644
--- a/src/Moltbot.Tray/UpdateDialog.cs
+++ b/src/Moltbot.Tray/UpdateDialog.cs
@@ -11,93 +11,43 @@ public enum UpdateDialogResult
Skip
}
-public class UpdateDialog : Form
+public class UpdateDialog : ModernForm
{
public UpdateDialogResult Result { get; private set; } = UpdateDialogResult.RemindLater;
public UpdateDialog(string version, string releaseNotes)
{
- Text = "Update Available - Moltbot Tray";
- Size = new Size(500, 400);
- StartPosition = FormStartPosition.CenterScreen;
- FormBorderStyle = FormBorderStyle.FixedDialog;
- MaximizeBox = false;
- MinimizeBox = false;
- Icon = SystemIcons.Information;
+ Text = "Update Available โ Moltbot Tray";
+ Size = new Size(500, 420);
+ Icon = IconHelper.GetLobsterIcon();
- var titleLabel = new Label
- {
- Text = "๐ฆ Update Available!",
- Font = new Font(Font.FontFamily, 14, FontStyle.Bold),
- Location = new Point(20, 20),
- AutoSize = true
- };
+ 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 = new Label
- {
- Text = $"Version {version} is ready to install",
- Location = new Point(20, 55),
- AutoSize = true
- };
+ var versionLabel = CreateModernLabel($"Version {version} is ready to install");
+ versionLabel.Location = new Point(20, 55);
Controls.Add(versionLabel);
- var notesLabel = new Label
- {
- Text = "Release Notes:",
- Font = new Font(Font.FontFamily, 9, FontStyle.Bold),
- Location = new Point(20, 85),
- AutoSize = true
- };
+ 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 = new TextBox
- {
- Text = string.IsNullOrWhiteSpace(releaseNotes) ? "No release notes available." : releaseNotes,
- Multiline = true,
- ReadOnly = true,
- ScrollBars = ScrollBars.Vertical,
- Location = new Point(20, 110),
- Size = new Size(440, 180),
- BackColor = SystemColors.Window
- };
+ 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 downloadButton = new Button
- {
- Text = "Download && Install",
- Size = new Size(130, 35),
- Location = new Point(20, 310),
- Font = new Font(Font.FontFamily, 9, FontStyle.Bold)
- };
- downloadButton.Click += (_, _) =>
- {
- Result = UpdateDialogResult.Download;
- DialogResult = DialogResult.OK;
- Close();
- };
- Controls.Add(downloadButton);
-
- var remindButton = new Button
- {
- Text = "Remind Me Later",
- Size = new Size(130, 35),
- Location = new Point(170, 310)
- };
- remindButton.Click += (_, _) =>
- {
- Result = UpdateDialogResult.RemindLater;
- DialogResult = DialogResult.Cancel;
- Close();
- };
- Controls.Add(remindButton);
-
- var skipButton = new Button
- {
- Text = "Skip This Version",
- Size = new Size(130, 35),
- Location = new Point(320, 310)
- };
+ var skipButton = CreateModernButton("Skip Version");
+ skipButton.Size = new Size(120, 36);
+ skipButton.Location = new Point(20, 330);
skipButton.Click += (_, _) =>
{
Result = UpdateDialogResult.Skip;
@@ -106,7 +56,30 @@ public class UpdateDialog : Form
};
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/Moltbot.Tray/WebChatForm.cs b/src/Moltbot.Tray/WebChatForm.cs
index 3d438c2..7a2f577 100644
--- a/src/Moltbot.Tray/WebChatForm.cs
+++ b/src/Moltbot.Tray/WebChatForm.cs
@@ -5,25 +5,23 @@ using System.Drawing;
using System.IO;
using System.Threading.Tasks;
using System.Windows.Forms;
+using Microsoft.Win32;
namespace MoltbotTray;
///
-/// Embeds the Moltbot WebChat UI via WebView2, matching the macOS native chat panel.
+/// Embeds the Moltbot WebChat UI via WebView2 with modern Windows 11 styling.
///
-public class WebChatForm : Form
+public class WebChatForm : ModernForm
{
private WebView2? _webView;
private readonly string _gatewayUrl;
private readonly string _token;
- private ToolStrip? _toolbar;
+ private Panel? _toolbar;
private bool _initialized;
private static WebChatForm? _instance;
- ///
- /// Show or focus the singleton WebChat window.
- ///
public static void ShowOrFocus(string gatewayUrl, string token)
{
if (_instance != null && !_instance.IsDisposed)
@@ -50,26 +48,28 @@ public class WebChatForm : Form
Text = "Moltbot Chat";
Size = new Size(520, 750);
MinimumSize = new Size(380, 450);
- StartPosition = FormStartPosition.CenterScreen;
+ FormBorderStyle = FormBorderStyle.Sizable;
Icon = IconHelper.GetLobsterIcon();
- BackColor = Color.FromArgb(30, 30, 30);
- // Toolbar
- _toolbar = new ToolStrip
+ // Modern toolbar panel - generous height for emoji rendering
+ _toolbar = new Panel
{
- GripStyle = ToolStripGripStyle.Hidden,
- RenderMode = ToolStripRenderMode.System,
- BackColor = Color.FromArgb(45, 45, 45),
- ForeColor = Color.White
+ Dock = DockStyle.Top,
+ Height = 50,
+ BackColor = SurfaceColor
};
- var homeBtn = new ToolStripButton("๐ Home") { ForeColor = Color.White };
+ var btnY = 8;
+ var homeBtn = CreateToolbarButton("๐ ", "Home");
+ homeBtn.Location = new Point(8, btnY);
homeBtn.Click += (_, _) => NavigateToChat();
- var refreshBtn = new ToolStripButton("โป Refresh") { ForeColor = Color.White };
+ var refreshBtn = CreateToolbarButton("โป", "Refresh");
+ refreshBtn.Location = new Point(50, btnY);
refreshBtn.Click += (_, _) => _webView?.Reload();
- var popoutBtn = new ToolStripButton("โ Browser") { ForeColor = Color.White };
+ var popoutBtn = CreateToolbarButton("โ", "Open in Browser");
+ popoutBtn.Location = new Point(92, btnY);
popoutBtn.Click += (_, _) =>
{
var url = _gatewayUrl.Replace("ws://", "http://").Replace("wss://", "https://");
@@ -77,43 +77,56 @@ public class WebChatForm : Form
catch { }
};
- var devToolsBtn = new ToolStripButton("๐ง DevTools") { ForeColor = Color.White };
+ var devToolsBtn = CreateToolbarButton("๐ง", "DevTools");
+ devToolsBtn.Location = new Point(134, btnY);
devToolsBtn.Click += (_, _) => _webView?.CoreWebView2?.OpenDevToolsWindow();
- _toolbar.Items.Add(homeBtn);
- _toolbar.Items.Add(refreshBtn);
- _toolbar.Items.Add(popoutBtn);
- _toolbar.Items.Add(new ToolStripSeparator());
- _toolbar.Items.Add(devToolsBtn);
+ _toolbar.Controls.AddRange(new Control[] { homeBtn, refreshBtn, popoutBtn, devToolsBtn });
// WebView2 fills remaining space
_webView = new WebView2
{
Dock = DockStyle.Fill,
- DefaultBackgroundColor = Color.FromArgb(30, 30, 30)
+ DefaultBackgroundColor = IsDarkMode ? Color.FromArgb(25, 25, 25) : Color.FromArgb(250, 250, 250)
};
- // Controls layout โ toolbar on top, webview fills rest
Controls.Add(_webView);
Controls.Add(_toolbar);
- _toolbar.Dock = DockStyle.Top;
+ }
+
+ 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
{
- // Use a dedicated user data folder
var userDataDir = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"MoltbotTray", "WebView2");
- var env = await CoreWebView2Environment.CreateAsync(
- userDataFolder: userDataDir);
-
+ var env = await CoreWebView2Environment.CreateAsync(userDataFolder: userDataDir);
await _webView!.EnsureCoreWebView2Async(env);
- // Configure WebView2
var settings = _webView.CoreWebView2.Settings;
settings.IsStatusBarEnabled = false;
settings.AreDefaultContextMenusEnabled = true;
@@ -121,7 +134,6 @@ public class WebChatForm : Form
_initialized = true;
Logger.Info("WebView2 initialized");
-
NavigateToChat();
}
catch (WebView2RuntimeNotFoundException)
@@ -155,12 +167,10 @@ public class WebChatForm : Form
{
if (!_initialized || _webView?.CoreWebView2 == null) return;
- // Convert ws:// to http:// for the web UI
var httpUrl = _gatewayUrl
.Replace("ws://", "http://")
.Replace("wss://", "https://");
- // The gateway serves WebChat at the root with token auth
var chatUrl = $"{httpUrl}?token={Uri.EscapeDataString(_token)}";
_webView.CoreWebView2.Navigate(chatUrl);
Logger.Info($"Navigating to WebChat: {httpUrl}");
@@ -174,3 +184,4 @@ public class WebChatForm : Form
}
}
+
diff --git a/src/Moltbot.Tray/WelcomeDialog.cs b/src/Moltbot.Tray/WelcomeDialog.cs
new file mode 100644
index 0000000..45ef1f5
--- /dev/null
+++ b/src/Moltbot.Tray/WelcomeDialog.cs
@@ -0,0 +1,95 @@
+using System;
+using System.Diagnostics;
+using System.Drawing;
+using System.Windows.Forms;
+
+namespace MoltbotTray;
+
+///
+/// First-run welcome dialog to help users get started with Moltbot.
+///
+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" +
+ "Moltbot 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 });
+ }
+}