From 40a68ec1001bedc8326effaaed21244e748a1d0b Mon Sep 17 00:00:00 2001 From: Scott Hanselman Date: Wed, 28 Jan 2026 22:15:59 -0800 Subject: [PATCH] Modern Windows 11 UI overhaul with Mac parity - 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 --- README.md | 46 ++- docs/molty1.png | Bin 0 -> 32963 bytes src/Moltbot.Shared/MoltbotGatewayClient.cs | 73 +++- src/Moltbot.Tray/DownloadProgressDialog.cs | 52 +-- src/Moltbot.Tray/ModernForm.cs | 257 ++++++++++++ src/Moltbot.Tray/ModernTrayMenu.cs | 435 ++++++++++++++++++++ src/Moltbot.Tray/NotificationHistoryForm.cs | 65 +-- src/Moltbot.Tray/QuickSendDialog.cs | 94 ++--- src/Moltbot.Tray/SettingsDialog.cs | 290 ++++++------- src/Moltbot.Tray/StatusDetailForm.cs | 62 +-- src/Moltbot.Tray/TrayApplication.cs | 296 ++++++++++++- src/Moltbot.Tray/UpdateDialog.cs | 119 +++--- src/Moltbot.Tray/WebChatForm.cs | 79 ++-- src/Moltbot.Tray/WelcomeDialog.cs | 95 +++++ 14 files changed, 1502 insertions(+), 461 deletions(-) create mode 100644 docs/molty1.png create mode 100644 src/Moltbot.Tray/ModernForm.cs create mode 100644 src/Moltbot.Tray/ModernTrayMenu.cs create mode 100644 src/Moltbot.Tray/WelcomeDialog.cs 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. +![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) diff --git a/docs/molty1.png b/docs/molty1.png new file mode 100644 index 0000000000000000000000000000000000000000..034b54433c003e45685b311b706773e789219d33 GIT binary patch literal 32963 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Ⓢ-CYyXOzowH8%-ymDZmJlq~aAqJG-+|l*GMtj_XUaOQnQ8>)X zgFWF%1iZBcsGh+%h2cFr!%Q%q;_8A*G&8lwGOpoAygbTR?${9^Z#+6(;ln&=OCz*k z?A?ODaceD;F_D(}RefjIXW}1lXihT<0=`2PAWgI#a%XtuOH<8f2NVhQoH)1`+9rzt zhatS&cFNl3d7>D?y8bQfFycXkvd%4b(gi0OnfU$uUL?QybIRD#g9 zvc^;M^qW-4JrDN5qf1BagUpN0e%+IHtK$%vNaS+c5vp;YxEg%&?RhQB_uHQdJ3DTL zf?E`DXHuE3mr;SM4*&FTXS9D>H@G|osVMwFZk)hPMImt$2VfhZmI$444%zn7B=9Hq;y3hQlGhsx$>1S3l{C2#*GJET6>}kPC9S zHt)}hd|AkJ20unWR3N?cOfaAjKxwgXFGiAW?eHNGBeq(40Tjlybd zXn7=)w-AO}+H_)+8pmyP7yMJ){!Smw3smD-XKu*yoZoPGM_JF|+6sfLr8kePb2e;u zEw4(vq}7}kZ{dRg@t_1p5D}2Z4gu+|)GX)VPLef?hfpa-l!kV(tWkm4um~kA0#)zb zHA7N%!K4k^HJ0oX;667FB#AcrObSmM9Qg2e4W@-=X1^(J9|&!N-{iQM2$10O;Buyv z&@%u5#C#vy@d4%4$_Q(*D9GRN*ve1*4iQtaQlHV{3-Ps)g|;iD7h)5X#8$Ovw01C} zT>@7EmUI$Z31fj7WmJnBn>6_gY&6Z41l|3yg_SgVxpATeH%6hZH)GdCa_%SUmW8#^ z1JFnR>`N+bnDf5!U)9d+ceUH4=hg@jA@3@?`WIPLt`eU9n=HaDpzFs6sRO8Z%2)tt z;8oxUtP$CJ8`^hI!P!$nPY;3Pw^~>z|HJSp|E3QUPR5J;ugQ~fM%1{Ng@cZA7RAHg zVGy%CUX+Tt)dO!CbhL#aquj;*17%?djNa@{<3NBTdu_bfUYQbq5_0cD9%P79auN z%}^X2!`%sFU3gsd2752g>krvIU#D z!{aD+iI-nIdkXJ7C8c0`*|Sjb7T1Y&Ym;&RqOS=i@uH75gqDY5_*N?p+Mar33ozY_ zN0h^x?iXlCQ5xyIY}X{}F60@yNR|4PzGexDVe@%u$auhlIuU zFj&)Cdy74kCC)2dS$Z;x1A>$Mbf#7Ehj_^ju0KFnfHS_q;nALX5Ngxlvr8N8qf-pj zosTm`kJ_`T5j9Es+xjlby50_O|)N{YyYQmvvu<-Mvc4PWH?D zi%pT4*n$s>KSsX+ad7e2!T0EqYq{ksGU!Bgo!48S8iI^Qh1LE+F}h?OFvvKg)Nf&@e?X+EV8v3@WTR)&(3{3$D<%BX@`!E>z$ z2Pxo%y2lLy)ybx=BwcJ$c>W+U7pPEnpuiaeBP5Y`m#c%Dz;Fj$o*INJp^3XErgKpv4yo?OWLFPNvLs+?;HQ zt-KgXNKW|CVp57wNS|3^bh6t8oQOadlt~_#&Ne?|z*U;ow5c}Ep-=^ly3Sg{)JAgt zup)O0SUg)f@jsXIp;dP#y|k|&m-A6D)$MNi6NTYf0I_oJSdqnjrz)YvZlTvF*Zlfe z0h)D<7q@x-v|GjK`NiF?H(2htN_k_QmnKv9p}#8>RFpkDHDqd|rWm(2gj*iceFS5f zG=xbUoZ$8_6{n$fjSGk!lU3@40SwVvvkpjyWP&0w+N1ijmXvzK0)k;Tu zDt1SwnJw};3aA4etQ?*vjVVC63G~Trx`L48Lz=-xEg8!IJk%*1wOt(t+G4v^t{WNV zazRN6mj0w%&%A(nXWN8uGDdSvhGvG)DnyYodv3;9?0G`|u+Hqv6|iDZ=^MlaK@QD( zs?oM74Yc!vDlnw-YZ0yPcYo9&Q5{8?WC@LLu^z0k1wc7i`F;p0 z>Y-K58CJ^Bv2LB{FPTtMJj){z6RFLb2VlAC^IcOXjNrs%YZ{qqA@;no47Xq$6Dm=; z_bcYO0O#>3qSb;VfKid>px^3-WVlM{V?FDw>?_^#puQ~^@^}^3KI`{PJ|8J*GV(b^ zE0@CzTM3k{M0j_Wc3Btce!P#bN&GWZCN+|351V86ti>%QcSgfeA9583!Tu>U);`-V zvwdy%#{$R-cyg)n03cy+}l0wNnKPXd>1>XOYGWD+} zN{2yA2AYIC2CDs2WsIBJa0H<(_o|MQ&$g}hX18i(>dD41lVj1S;zNscRS?Y5xZf`P zow(`P6U$Zj(ZY={VC!FZOm_O&Y7@Wah4#7NJ!ooQbO+2()LE&%`)LI?M$g`)U=kzW znvTyI;H3HnNQvuQmL0G$v0PmS%8p7vdEb8lv=m+5{o!w5>cv>QX+%*>n^3c<0%*=DaEBs5ffw+;1pmyy zs;KrOwFrf=_F7?$`6pb1%N183pZC~SPBN>*B02B4Q}0+&o<1kCQ7A~JsIkwdW=S2KPR*LQu^Y0e^)mV^P5;CJw5iAxgx|dA@@S8 zIBDPzy>(SOaWf=3V+?21&6&eZ_M*(IfGbi`%Lxc-mrx`%Q2!z-%KQltcJCa9Dy1u_=l!KdofZ=tI-+s(0RyZB}m)S9o&~+<#M`u-S#8>McTvqP}4Zh zwNZJmv_tkY_r$go9}>)n7jn|gCT2bHHO1=ozT9yYN#LbG#8P0V<=+@2sNmVHX@qmc z26i#H4YIz3=!4P?15ruD|+LX0hYx*XC zpY0|;Q9_FyB(|hZzwr0$cAV`Z)l19TPiW%`$yOpLl3j{*Gv245xPg|1*bp^CDK?y4 zi#7*)=A^LOdBG&+vEelr^>ro02UqkS(A~Ga`{> z<90Fw>WM#%;R?`0go4M#7#YyLiIyQ?tDdZ&&~Ep)7K>;YG1duUhu=Sz;DGFI;uuCZ zw2Xj-Bcon4*9Cr{F-_#A=qGf13ZZS)YaLPbdU7u$)8H_4I~OgJfz$=N9rVrp1&1r? zN>{hZjVW{)b3I#lLOhdU-+D2jYQ8t)96{Q(odv{{xDlKE!jaq|WQABVG*gBk+Zl=P zuOJi7Vw)l+eOeKF4eS#*fRULd2T)EOkT1B4z&}{_2>WiK>gj|nI|reKq%%GB%Tpzk zzS-c(e&iP&5N5G`i7orx+UjZrKSptGas7HS0=#nnX9;5esx__+y}06e2V}i_O9S_d z2bj=z@1J=2AaxzXM5tXw9ExKS*p1G!=f7BZcKrlwMagByPt(5|&t!E$LVZ?Ml}WlD zCKrD9iv>uFZz-HQ`SA!7W{(MT@980@NRz?@!DG_hK@z<#4ENK*-N#F5k4EU!;oM^& z>wW9fwGEc!9xRCY*!Va&o!f)VU5|VB{{0qUN2WmxL_)eI{Ju>6$=*2TzoFm04L>fk zuCA<1!4YY+*3*NUTUcy`$sc2tS=P6J_te(muB*)h@cv%gSs9c9KftG#SA%LMpAf%) z|9)g-L`32{5g@c|39y^={0@!o-@l((Utf=|h*y=n&BpEG>>SI@^z=Y=yeC<>91uI0 gA13eM!0z-6$0~$Rex-qk1OonOsOZ99D_IBrAK1_RPyhe` literal 0 HcmV?d00001 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 }); + } +}