From 7c9b9f50968279169bbc4f4ed496073c945b6afc Mon Sep 17 00:00:00 2001 From: Junho Yeo Date: Tue, 16 Dec 2025 21:02:38 +0900 Subject: [PATCH] fix(session-notification): Replace blocking MessageBox with native toast on Windows (#62) The previous Windows implementation used System.Windows.Forms.MessageBox which displays a blocking modal dialog requiring user interaction. This replaces it with the native Windows.UI.Notifications.ToastNotificationManager API (Windows 10+) which shows a non-intrusive toast notification in the corner, consistent with macOS and Linux behavior. - Uses native Toast API (no external dependencies like BurntToast) - Non-blocking: notification auto-dismisses - Graceful degradation: silently fails on older Windows versions - Fix escaping for each platform (PowerShell: '' for quotes, AppleScript: backslash) --- src/hooks/session-notification.ts | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/src/hooks/session-notification.ts b/src/hooks/session-notification.ts index 21b9732..d9ff5a8 100644 --- a/src/hooks/session-notification.ts +++ b/src/hooks/session-notification.ts @@ -48,19 +48,34 @@ async function sendNotification( title: string, message: string ): Promise { - const escapedTitle = title.replace(/"/g, '\\"').replace(/'/g, "\\'") - const escapedMessage = message.replace(/"/g, '\\"').replace(/'/g, "\\'") - switch (p) { - case "darwin": - await ctx.$`osascript -e ${"display notification \"" + escapedMessage + "\" with title \"" + escapedTitle + "\""}` + case "darwin": { + const esTitle = title.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + const esMessage = message.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + await ctx.$`osascript -e ${"display notification \"" + esMessage + "\" with title \"" + esTitle + "\""}` break + } case "linux": - await ctx.$`notify-send ${escapedTitle} ${escapedMessage} 2>/dev/null`.catch(() => {}) + await ctx.$`notify-send ${title} ${message} 2>/dev/null`.catch(() => {}) break - case "win32": - await ctx.$`powershell -Command ${"[System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms'); [System.Windows.Forms.MessageBox]::Show('" + escapedMessage + "', '" + escapedTitle + "')"}` + case "win32": { + const psTitle = title.replace(/'/g, "''") + const psMessage = message.replace(/'/g, "''") + const toastScript = ` +[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null +$Template = [Windows.UI.Notifications.ToastNotificationManager]::GetTemplateContent([Windows.UI.Notifications.ToastTemplateType]::ToastText02) +$RawXml = [xml] $Template.GetXml() +($RawXml.toast.visual.binding.text | Where-Object {$_.id -eq '1'}).AppendChild($RawXml.CreateTextNode('${psTitle}')) | Out-Null +($RawXml.toast.visual.binding.text | Where-Object {$_.id -eq '2'}).AppendChild($RawXml.CreateTextNode('${psMessage}')) | Out-Null +$SerializedXml = New-Object Windows.Data.Xml.Dom.XmlDocument +$SerializedXml.LoadXml($RawXml.OuterXml) +$Toast = [Windows.UI.Notifications.ToastNotification]::new($SerializedXml) +$Notifier = [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('OpenCode') +$Notifier.Show($Toast) +`.trim().replace(/\n/g, "; ") + await ctx.$`powershell -Command ${toastScript}`.catch(() => {}) break + } } }