From 14b0d66e8a610581c37ebbbbbd4417db0f7f4c39 Mon Sep 17 00:00:00 2001 From: Hannes Rudolph Date: Tue, 26 Aug 2025 17:26:56 -0600 Subject: [PATCH] refactor: use terminal-notifier for macOS and node-notifier for Windows/Linux - macOS: Uses terminal-notifier (native tool) with fallback to osascript - Windows/Linux: Uses node-notifier with fallback to native tools - Removed VSCode notification API to use only native OS notifications - No compilation issues as terminal-notifier doesn't require building --- pnpm-lock.yaml | 6 +- src/integrations/notifications/index.ts | 183 ++++++++++++++++-------- src/package.json | 2 +- 3 files changed, 125 insertions(+), 66 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 136030119b..bc223d8760 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -598,6 +598,9 @@ importers: '@types/lodash.debounce': specifier: ^4.0.9 version: 4.0.9 + '@types/node-notifier': + specifier: ^8.0.5 + version: 8.0.5 '@vscode/codicons': specifier: ^0.0.36 version: 0.0.36 @@ -815,9 +818,6 @@ importers: '@types/node-ipc': specifier: ^9.2.3 version: 9.2.3 - '@types/node-notifier': - specifier: ^8.0.5 - version: 8.0.5 '@types/proper-lockfile': specifier: ^4.1.4 version: 4.1.4 diff --git a/src/integrations/notifications/index.ts b/src/integrations/notifications/index.ts index 41192b6a4a..efe8dc720c 100644 --- a/src/integrations/notifications/index.ts +++ b/src/integrations/notifications/index.ts @@ -14,20 +14,118 @@ async function showMacOSNotification(options: NotificationOptions): Promise str.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/'/g, "\\'") + const script = `display notification "${escape(message)}" with title "${escape(title)}" subtitle "${escape(subtitle)}" sound name "default"` + + try { + await execa("osascript", ["-e", script]) + } catch (osascriptError) { + console.error("Failed to show macOS notification:", osascriptError) + throw new Error(`Failed to show macOS notification: ${osascriptError}`) + } + } +} + +async function showWindowsNotification(options: NotificationOptions): Promise { + const { title = "Roo Code", subtitle = "", message } = options + + try { + // Use node-notifier for Windows + const notifier = await import("node-notifier") const iconPath = path.join(__dirname, "..", "..", "assets", "icons", "icon.png") + // Windows notification doesn't support subtitle, so combine it with message + const fullMessage = subtitle ? `${subtitle}\n${message}` : message + await new Promise((resolve, reject) => { notifier.notify( { title: title, - subtitle: subtitle, - message: message, - sound: "Tink", + message: fullMessage, icon: iconPath, + sound: true, + wait: false, + appID: "Roo Code", + }, + (error: Error | null) => { + if (error) { + reject(error) + } else { + resolve() + } + }, + ) + }) + } catch (error) { + // Fallback to PowerShell if node-notifier fails + console.warn("node-notifier failed, falling back to PowerShell:", error) + + const script = ` + [Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null + [Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] | Out-Null + + $template = @" + + + + ${subtitle} + ${message} + + + +"@ + + $xml = New-Object Windows.Data.Xml.Dom.XmlDocument + $xml.LoadXml($template) + $toast = [Windows.UI.Notifications.ToastNotification]::new($xml) + [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier("Roo Code").Show($toast) + ` + + try { + await execa("powershell", ["-Command", script]) + } catch (psError) { + throw new Error(`Failed to show Windows notification: ${psError}`) + } + } +} + +async function showLinuxNotification(options: NotificationOptions): Promise { + const { title = "Roo Code", subtitle = "", message } = options + + try { + // Use node-notifier for Linux + const notifier = await import("node-notifier") + const iconPath = path.join(__dirname, "..", "..", "assets", "icons", "icon.png") + + // Combine subtitle and message for node-notifier + const fullMessage = subtitle ? `${subtitle}\n${message}` : message + + await new Promise((resolve, reject) => { + notifier.notify( + { + title: title, + message: fullMessage, + icon: iconPath, + sound: true, wait: false, }, (error: Error | null) => { @@ -40,69 +138,30 @@ async function showMacOSNotification(options: NotificationOptions): Promise str.replace(/\\/g, "\\\\").replace(/"/g, '\\"') - const script = `display notification "${escape(message)}" with title "${escape(title)}" subtitle "${escape(subtitle)}" sound name "Tink"` + // Combine subtitle and message if subtitle exists + const fullMessage = subtitle ? `${subtitle}\n${message}` : message try { - await execa("osascript", ["-e", script]) - } catch (osascriptError) { - throw new Error(`Failed to show macOS notification: ${osascriptError}`) + await execa("notify-send", [ + title, + fullMessage, + "-i", + path.join(__dirname, "..", "..", "assets", "icons", "icon.png"), + ]) + } catch (notifySendError: any) { + if (notifySendError.code === "ENOENT") { + throw new Error( + "notify-send is not installed. Please install libnotify-bin (apt install libnotify-bin on Debian/Ubuntu)", + ) + } + throw new Error(`Failed to show Linux notification: ${notifySendError}`) } } } -async function showWindowsNotification(options: NotificationOptions): Promise { - const { subtitle, message } = options - - const script = ` - [Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null - [Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] | Out-Null - - $template = @" - - - - ${subtitle} - ${message} - - - -"@ - - $xml = New-Object Windows.Data.Xml.Dom.XmlDocument - $xml.LoadXml($template) - $toast = [Windows.UI.Notifications.ToastNotification]::new($xml) - [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier("Roo Code").Show($toast) - ` - - try { - await execa("powershell", ["-Command", script]) - } catch (error) { - throw new Error(`Failed to show Windows notification: ${error}`) - } -} - -async function showLinuxNotification(options: NotificationOptions): Promise { - const { title = "", subtitle = "", message } = options - - // Combine subtitle and message if subtitle exists - const fullMessage = subtitle ? `${subtitle}\n${message}` : message - - try { - await execa("notify-send", [title, fullMessage]) - } catch (error: any) { - if (error.code === "ENOENT") { - throw new Error( - "notify-send is not installed. Please install libnotify-bin (apt install libnotify-bin on Debian/Ubuntu)", - ) - } - throw new Error(`Failed to show Linux notification: ${error}`) - } -} - export async function showSystemNotification(options: NotificationOptions): Promise { try { if (vscode.window.state.focused && !options.force) { diff --git a/src/package.json b/src/package.json index 4df3ce84f7..bfc9ad7476 100644 --- a/src/package.json +++ b/src/package.json @@ -437,6 +437,7 @@ "@roo-code/telemetry": "workspace:^", "@roo-code/types": "workspace:^", "@types/lodash.debounce": "^4.0.9", + "@types/node-notifier": "^8.0.5", "@vscode/codicons": "^0.0.36", "async-mutex": "^0.5.0", "axios": "^1.7.4", @@ -511,7 +512,6 @@ "@types/node": "20.x", "@types/node-cache": "^4.1.3", "@types/node-ipc": "^9.2.3", - "@types/node-notifier": "^8.0.5", "@types/proper-lockfile": "^4.1.4", "@types/ps-tree": "^1.1.6", "@types/stream-json": "^1.7.8",