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
This commit is contained in:
Hannes Rudolph 2025-08-26 17:26:56 -06:00
parent cb3941ff65
commit 14b0d66e8a
3 changed files with 125 additions and 66 deletions

6
pnpm-lock.yaml generated
View file

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

View file

@ -14,20 +14,118 @@ async function showMacOSNotification(options: NotificationOptions): Promise<void
const { title = "Roo Code", subtitle = "", message } = options
try {
// Try to use node-notifier first (more reliable cross-platform solution)
const notifier = await import("node-notifier")
// First try terminal-notifier (native macOS tool, no compilation needed)
await execa("terminal-notifier", [
"-title",
title,
"-subtitle",
subtitle || "",
"-message",
message,
"-sound",
"default",
"-group",
"com.roocode.vscode",
"-appIcon",
path.join(__dirname, "..", "..", "assets", "icons", "icon.png"),
])
} catch (terminalNotifierError) {
// If terminal-notifier is not available, fall back to osascript
console.log("terminal-notifier not available, falling back to osascript")
// Get the extension's icon path
const escape = (str: string = "") => 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<void> {
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<void>((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 = @"
<toast>
<visual>
<binding template="ToastText02">
<text id="1">${subtitle}</text>
<text id="2">${message}</text>
</binding>
</visual>
</toast>
"@
$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<void> {
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<void>((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<void
)
})
} catch (error) {
// Fallback to osascript if node-notifier fails
console.warn("node-notifier failed, falling back to osascript:", error)
// Fallback to notify-send if node-notifier fails
console.warn("node-notifier failed, falling back to notify-send:", error)
const escape = (str: string = "") => 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<void> {
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 = @"
<toast>
<visual>
<binding template="ToastText02">
<text id="1">${subtitle}</text>
<text id="2">${message}</text>
</binding>
</visual>
</toast>
"@
$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<void> {
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<void> {
try {
if (vscode.window.state.focused && !options.force) {

View file

@ -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",