Modern Windows 11 UI overhaul with Mac parity
Some checks failed
Build and Test / test (push) Has been cancelled
Build and Test / build-extension (x64) (push) Has been cancelled
Build and Test / build (win-arm64) (push) Has been cancelled
Build and Test / build (win-x64) (push) Has been cancelled
Build and Test / build-extension (arm64) (push) Has been cancelled
Build and Test / release (push) Has been cancelled

- New ModernTrayMenu: Windows 11-style flyout replacing ContextMenuStrip
  - Dark/light mode auto-detection
  - Lobster branding header with accent colors
  - Clickable channel toggles (start/stop Telegram/WhatsApp)
  - Sessions link to /sessions, Cron Jobs to /cron
  - Status badges with color coding (READY/IDLE/ON/OFF)

- New ModernForm base class for all dialogs
  - Rounded corners via DWM APIs
  - Consistent theming across Settings, QuickSend, WebChat, etc.
  - Accent color support

- New WelcomeDialog for first-run experience
  - Guides users to get API token
  - Links to docs.molt.bot documentation
  - Opens Settings after onboarding

- Channel status parity: unified READY status for linked channels
- Service Health menu item (replaces Run Health Check)
- Test Notification button in Settings
- Various DPI and spacing fixes

- Updated README with screenshot and expanded feature list
This commit is contained in:
Scott Hanselman 2026-01-28 22:15:59 -08:00
parent df3e5508a4
commit 40a68ec100
14 changed files with 1502 additions and 461 deletions

View File

@ -2,6 +2,8 @@
A Windows companion suite for [Moltbot](https://moltbot.com) - the AI-powered personal assistant.
![Molty - Windows Tray App](docs/molty1.png)
## 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)

BIN
docs/molty1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View File

@ -163,6 +163,54 @@ public class MoltbotGatewayClient : IDisposable
catch { }
}
/// <summary>Start a channel (telegram, whatsapp, etc).</summary>
public async Task<bool> 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;
}
}
/// <summary>Stop a channel (telegram, whatsapp, etc).</summary>
public async Task<bool> 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";

View File

@ -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);
}
}

View File

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

View File

@ -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;
/// <summary>
/// Modern flyout menu with Windows 11 styling - dark/light mode, rounded corners, acrylic blur.
/// Replaces the dated ContextMenuStrip with a custom-drawn popup.
/// </summary>
public class ModernTrayMenu : Form
{
// DWM APIs for acrylic/mica effect
[DllImport("dwmapi.dll")]
private static extern int DwmSetWindowAttribute(IntPtr hwnd, int attr, ref int attrValue, int attrSize);
[DllImport("dwmapi.dll")]
private static extern int DwmExtendFrameIntoClientArea(IntPtr hWnd, ref MARGINS pMarInset);
[StructLayout(LayoutKind.Sequential)]
private struct MARGINS { public int Left, Right, Top, Bottom; }
private const int DWMWA_USE_IMMERSIVE_DARK_MODE = 20;
private const int DWMWA_WINDOW_CORNER_PREFERENCE = 33;
private const int DWMWA_SYSTEMBACKDROP_TYPE = 38;
private const int DWMWCP_ROUND = 2;
private const int DWMSBT_TRANSIENTWINDOW = 3; // Acrylic
// Theme colors
private bool _isDarkMode;
private Color _backgroundColor;
private Color _foregroundColor;
private Color _hoverColor;
private Color _accentColor;
private Color _separatorColor;
private Color _subtleTextColor;
// Menu items
private readonly List<ModernMenuItem> _items = new();
private int _hoveredIndex = -1;
private const int ItemHeight = 36;
private const int IconWidth = 32; // Wider for emoji
private const int Padding = 16; // More padding
private const int CornerRadius = 8;
public event EventHandler<string>? MenuItemClicked;
public ModernTrayMenu()
{
SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint | ControlStyles.DoubleBuffer, true);
FormBorderStyle = FormBorderStyle.None;
ShowInTaskbar = false;
TopMost = true;
StartPosition = FormStartPosition.Manual;
// Detect theme (styling applied in OnHandleCreated)
DetectTheme();
// Track mouse for hover effects
MouseMove += OnMouseMove;
MouseLeave += (_, _) => { _hoveredIndex = -1; Invalidate(); };
MouseClick += OnMouseClick;
// Close when clicking outside
Deactivate += (_, _) => Hide();
}
protected override void OnHandleCreated(EventArgs e)
{
base.OnHandleCreated(e);
ApplyModernStyling();
}
private void DetectTheme()
{
try
{
using var key = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize");
var value = key?.GetValue("AppsUseLightTheme");
_isDarkMode = value is int i && i == 0;
}
catch
{
_isDarkMode = false;
}
if (_isDarkMode)
{
_backgroundColor = Color.FromArgb(32, 32, 32);
_foregroundColor = Color.FromArgb(255, 255, 255);
_hoverColor = Color.FromArgb(45, 45, 48);
_accentColor = Color.FromArgb(255, 99, 71); // Lobster red
_separatorColor = Color.FromArgb(80, 80, 80);
_subtleTextColor = Color.FromArgb(180, 180, 180);
}
else
{
_backgroundColor = Color.FromArgb(249, 249, 249);
_foregroundColor = Color.FromArgb(28, 28, 28);
_hoverColor = Color.FromArgb(229, 229, 229);
_accentColor = Color.FromArgb(220, 53, 53); // Lobster red
_separatorColor = Color.FromArgb(200, 200, 200);
_subtleTextColor = Color.FromArgb(100, 100, 100);
}
BackColor = _backgroundColor;
}
private void ApplyModernStyling()
{
// Enable Windows 11 rounded corners
int preference = DWMWCP_ROUND;
DwmSetWindowAttribute(Handle, DWMWA_WINDOW_CORNER_PREFERENCE, ref preference, sizeof(int));
// Enable dark mode for title bar (affects some rendering)
int darkMode = _isDarkMode ? 1 : 0;
DwmSetWindowAttribute(Handle, DWMWA_USE_IMMERSIVE_DARK_MODE, ref darkMode, sizeof(int));
// Try to enable acrylic backdrop (Windows 11 22H2+)
int backdropType = DWMSBT_TRANSIENTWINDOW;
DwmSetWindowAttribute(Handle, DWMWA_SYSTEMBACKDROP_TYPE, ref backdropType, sizeof(int));
}
public void ClearItems() => _items.Clear();
public void AddBrandHeader(string icon, string text)
{
_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; }
}
}

View File

@ -6,9 +6,9 @@ using System.Windows.Forms;
namespace MoltbotTray;
/// <summary>
/// Shows recent notification history in a simple list view.
/// Shows recent notification history in a modern styled list view.
/// </summary>
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
}
}

View File

@ -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);
}
}

View File

@ -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()

View File

@ -9,7 +9,7 @@ namespace MoltbotTray;
/// <summary>
/// Shows detailed gateway status, sessions, channels, and usage in a rich view.
/// </summary>
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
}
}

View File

@ -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<ToolStripItem> _channelItems = new();
private readonly List<ToolStripItem> _sessionItems = new();
// Channel and session data for modern menu
private ChannelHealth[] _lastChannels = Array.Empty<ChannelHealth>();
private SessionInfo[] _lastSessions = Array.Empty<SessionInfo>();
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<string>();
_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();
}
}

View File

@ -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;
}
}

View File

@ -5,25 +5,23 @@ using System.Drawing;
using System.IO;
using System.Threading.Tasks;
using System.Windows.Forms;
using Microsoft.Win32;
namespace MoltbotTray;
/// <summary>
/// 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.
/// </summary>
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;
/// <summary>
/// Show or focus the singleton WebChat window.
/// </summary>
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
}
}

View File

@ -0,0 +1,95 @@
using System;
using System.Diagnostics;
using System.Drawing;
using System.Windows.Forms;
namespace MoltbotTray;
/// <summary>
/// First-run welcome dialog to help users get started with Moltbot.
/// </summary>
public class WelcomeDialog : ModernForm
{
private readonly string _dashboardUrl;
public WelcomeDialog(string dashboardUrl)
{
_dashboardUrl = dashboardUrl;
InitializeComponent();
}
private void InitializeComponent()
{
Text = "Welcome to Molty";
Size = new Size(500, 380);
FormBorderStyle = FormBorderStyle.FixedDialog;
MaximizeBox = false;
MinimizeBox = false;
StartPosition = FormStartPosition.CenterScreen;
Icon = IconHelper.GetLobsterIcon();
var y = 20;
// Lobster header
var headerLabel = new Label
{
Text = "🦞",
Font = new Font("Segoe UI Emoji", 36),
Location = new Point(0, y),
Size = new Size(ClientSize.Width, 60),
TextAlign = ContentAlignment.MiddleCenter,
ForeColor = AccentColor
};
y += 70;
// Welcome text
var welcomeLabel = new Label
{
Text = "Welcome to Molty!",
Font = new Font("Segoe UI", 14, FontStyle.Bold),
Location = new Point(0, y),
Size = new Size(ClientSize.Width, 30),
TextAlign = ContentAlignment.MiddleCenter,
ForeColor = ForegroundColor,
BackColor = Color.Transparent
};
y += 40;
// Instructions
var instructionsLabel = CreateModernLabel(
"To get started, you'll need an API token from your\n" +
"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 });
}
}